summaryrefslogtreecommitdiffstats
path: root/config-model/src
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /config-model/src
Publish
Diffstat (limited to 'config-model/src')
-rw-r--r--config-model/src/main/assembly/.gitignore0
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ApplicationConfigProducerRoot.java279
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/CommonConfigsProducer.java34
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ConfigModel.java64
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ConfigModelContext.java72
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ConfigModelInstanceFactory.java20
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ConfigModelRegistry.java51
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ConfigModelRepo.java256
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ConfigModelRepoAdder.java16
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/ConfigModelUtils.java64
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/MapConfigModelRegistry.java63
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/NullConfigModelRegistry.java21
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/admin/AdminModel.java113
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelBuilder.java119
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelId.java94
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/builder/xml/XmlHelper.java135
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/builder/xml/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/ConfigDefinitionStore.java15
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/DeployProperties.java115
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java381
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/SearchDocumentModel.java53
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/deploy/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/graph/ModelGraph.java80
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/graph/ModelGraphBuilder.java52
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/graph/ModelNode.java135
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducer.java448
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducerRoot.java67
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/producer/UserConfigRepo.java109
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/producer/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/Host.java40
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/Hosts.java120
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/HostsXmlProvisioner.java64
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java164
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/SingleNodeProvisioner.java49
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/provision/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/test/ConfigModelTestUtil.java40
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java243
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/test/MockRoot.java169
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/test/TestDriver.java111
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/test/TestRoot.java66
-rw-r--r--config-model/src/main/java/com/yahoo/config/model/test/package-info.java6
-rw-r--r--config-model/src/main/java/com/yahoo/documentmodel/DataTypeCollection.java15
-rw-r--r--config-model/src/main/java/com/yahoo/documentmodel/DataTypeRepo.java58
-rw-r--r--config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeCollection.java13
-rw-r--r--config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeRepo.java39
-rw-r--r--config-model/src/main/java/com/yahoo/documentmodel/NewDocumentType.java366
-rw-r--r--config-model/src/main/java/com/yahoo/documentmodel/VespaDocumentType.java39
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/ConstantTensorTransformer.java74
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/DefaultRankProfile.java140
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/DocumentModelBuilder.java415
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplier.java31
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForSearch.java21
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForStructs.java48
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/FieldSets.java65
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/Index.java203
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/MacroInliner.java39
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java968
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/RankProfileRegistry.java90
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/SDDocumentTypeOrderer.java126
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/Search.java555
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java429
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/UnprocessingSearchBuilder.java40
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/UnproperSearch.java31
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/UnrankedRankProfile.java28
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/AttributeFields.java157
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/Derived.java136
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java184
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/Deriver.java82
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/Exportable.java26
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldRankSettings.java79
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldResultTransform.java54
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/Index.java64
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexInfo.java559
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java231
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexingScript.java146
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/Juniperrc.java61
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinition.java44
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionSet.java93
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeTable.java72
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/RankProfileList.java62
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java374
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/SearchOrderer.java105
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/Summaries.java37
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClass.java144
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClassField.java110
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryMap.java111
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmFields.java275
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmSummary.java107
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/IndexStructureValidator.java50
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validation.java12
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validator.java23
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/Attribute.java320
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/BooleanIndexDefinition.java73
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/FieldSet.java42
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/Matching.java151
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/NormalizeLevel.java87
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/RankType.java45
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/Ranking.java68
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/SDDocumentType.java316
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/SDField.java770
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/Sorting.java64
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/Stemming.java72
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDDocumentType.java13
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDField.java19
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/TypedKey.java20
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/SDAnnotationType.java35
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/TemporaryAnnotationReferenceDataType.java26
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AliasOperation.java40
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AttributeOperation.java153
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BodyOperation.java14
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BoldingOperation.java24
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperation.java11
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperationContainer.java13
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/HeaderOperation.java14
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IdOperation.java33
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java114
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingOperation.java53
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingRewriteOperation.java12
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/MatchOperation.java53
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/NormalizingOperation.java32
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/QueryCommandOperation.java23
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankOperation.java35
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankTypeOperation.java41
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SortingOperation.java91
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StemmingOperation.java24
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StructFieldOperation.java50
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldLongOperation.java81
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldOperation.java44
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldShortOperation.java30
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryToOperation.java39
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightOperation.java23
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightedSetOperation.java63
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/parser/SimpleCharStream.java16
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/AddExtraFieldsToDocument.java78
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributeProperties.java70
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributesImplicitWord.java47
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/Bolding.java46
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/BuiltInFieldSets.java49
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/CreatePositionZCurve.java201
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/DeprecateAttributePrefetch.java31
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/DisallowComplexMapAndWsetKeyTypes.java43
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/DiversitySettingsValidator.java58
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/ExactMatch.java98
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/FieldSetValidity.java94
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/FilterFieldNames.java70
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaries.java222
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFields.java38
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexFieldNames.java43
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexSettingsNonFieldNames.java47
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexTo2FieldSet.java33
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingInputs.java110
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingOutputs.java141
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValidation.java149
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValues.java69
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/IntegerIndex2Attribute.java86
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/LiteralBoost.java79
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeAliases.java60
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeDefaultSummaryTheSuperSet.java47
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchConsistency.java67
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidator.java92
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/MultifieldIndexHarmonizer.java82
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/NGramMatch.java76
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/OptimizeIlscript.java33
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/PredicateProcessor.java138
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java84
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/Processor.java131
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/ReservedDocumentNames.java37
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SearchMustHaveDocument.java28
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SetLanguage.java55
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SetRankTypeEmptyOnFilters.java30
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SortingSettings.java40
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/StringSettingsOnNonStringFields.java40
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryConsistency.java102
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryDynamicStructsArrays.java45
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSource.java73
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryNamesFieldCollisions.java57
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/TagType.java45
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/TensorFieldProcessor.java63
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/TextMatch.java122
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/TypedTransformProvider.java61
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/UriHack.java67
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/UrlFieldValidator.java32
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldTypes.java69
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldWithIndexSettingsCreatesIndex.java42
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/WordMatch.java38
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/IndexCommandResolver.java62
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/MultiFieldResolver.java33
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java85
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankTypeResolver.java46
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/StemmingResolver.java48
-rw-r--r--config-model/src/main/java/com/yahoo/searchdefinition/processing/package-info.java14
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentManager.java156
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentTypes.java158
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentModel.java31
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentSummary.java79
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/FieldView.java61
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchDef.java126
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchField.java71
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchManager.java27
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryField.java350
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java99
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/AbstractService.java516
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/Affinity.java37
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/Client.java25
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/ConfigProducer.java84
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/ConfigProducerRoot.java52
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/ConfigProxy.java58
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/ConfigSentinel.java107
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/Host.java121
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/HostResource.java251
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java203
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/InstanceResolver.java196
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/Logd.java34
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/PlainFormatter.java28
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/PortsMeta.java168
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/RecentLogFilter.java31
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/Service.java142
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/ServiceProvider.java11
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/SimpleConfigProducer.java31
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/VespaConfigModelRegistry.java50
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java557
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java196
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/AbstractMonitoringSystem.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java248
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/Configserver.java145
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/DefaultMetricConsumers.java294
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/FileDistributionOptions.java49
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/Logserver.java75
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/Metric.java99
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/MetricsConsumer.java35
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/ModelConfigProvider.java19
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/MonitoringSystem.java24
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/Slobrok.java77
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/Yamas.java19
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/ZooKeepersConfigProvider.java47
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerCluster.java77
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerComponent.java20
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerConfigurer.java47
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java110
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/admin/package-info.java17
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/ComponentValidator.java88
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidator.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexes.java54
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/RankSetupValidator.java159
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/RestartConfigs.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingSelectorValidator.java33
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingValidator.java27
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidator.java86
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/StreamingValidator.java69
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java81
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationId.java42
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationOverrides.java104
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validator.java23
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ChangeValidator.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java52
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidator.java162
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidator.java47
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidator.java34
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidator.java98
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidator.java54
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidator.java54
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaConfigChangeAction.java66
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRefeedAction.java84
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRestartAction.java36
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java116
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/ChangeMessageBuilder.java39
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidator.java60
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidator.java160
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeMessageBuilder.java97
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidator.java89
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/application/validation/xml/ValidationOverridesXMLReader.java57
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/UserConfigBuilder.java77
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/VespaModelBuilder.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryScaledAmountParser.java37
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryUnit.java38
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java101
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2Builder.java197
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java138
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientProviderBuilder.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientsBuilder.java38
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilder.java56
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilder.java243
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContainerClusterBuilder.java109
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilder.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFileDistributionOptionsBuilder.java53
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFilterBuilder.java18
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomHandlerBuilder.java48
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomMetricBuilderHelper.java47
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomRoutingBuilder.java138
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilder.java247
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomV20ClientsBuilder.java576
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilder.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java211
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java105
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ServletBuilder.java55
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilder.java301
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainSpecificationBuilder.java55
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainedComponentModelBuilder.java25
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainsBuilder.java56
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ComponentsBuilder.java156
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilder.java38
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomBuilderCreator.java26
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainBuilderBase.java51
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainsBuilder.java102
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/GenericChainedComponentModelBuilder.java35
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/InheritanceBuilder.java64
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocprocChainsBuilder.java33
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocumentProcessorModelBuilder.java54
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainBuilder.java33
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainsBuilder.java38
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocumentProcessorBuilder.java21
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingBuilder.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingChainBuilder.java31
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessorBuilder.java23
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/ProcessingChainsBuilder.java33
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilder.java88
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomGenericTargetBuilder.java35
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilder.java283
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainBuilder.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilder.java45
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilder.java22
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSourceBuilder.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/FederationOptionsBuilder.java60
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/SearchChainsBuilder.java34
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/TimeParser.java43
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/clients/Clients.java77
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/clients/ContainerDocumentApi.java165
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/clients/HttpGatewayOwner.java20
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolMaster.java29
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpooler.java31
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerProducer.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerService.java42
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/Container.java408
-rwxr-xr-xconfig-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java816
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/ContainerModel.java64
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java99
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/Component.java67
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentGroup.java19
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentsConfigGenerator.java59
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/ConfigProducerGroup.java65
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/ContainerSubsystem.java31
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/DiscBindingsConfigGenerator.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/FileStatusHandlerComponent.java26
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java59
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/HttpFilter.java36
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/Servlet.java23
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/ServletProvider.java38
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/SimpleComponent.java25
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/StatisticsComponent.java33
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chain.java81
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponent.java35
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponentConfigGenerator.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chains.java107
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainsConfigGenerator.java60
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ProcessingHandler.java34
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/component/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java181
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java44
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/package-info.java8
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/docproc/ContainerDocproc.java178
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChain.java43
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChains.java53
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocumentProcessor.java35
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/docproc/MbusClient.java43
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/docproc/model/DocumentProcessorModel.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java149
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/Filter.java24
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterChains.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterConfigProvider.java61
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/Http.java123
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java73
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterBuilder.java43
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainBuilder.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainsBuilder.java49
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/HttpBuilder.java83
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java27
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/jersey/Jersey2Servlet.java35
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/jersey/JerseyHandler.java28
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApi.java84
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApiContext.java149
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/jersey/xml/RestApiBuilder.java71
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChain.java19
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChains.java25
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/processing/Processor.java20
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerHttpGateway.java22
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerSearch.java204
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/DeclaredQueryProfileVariants.java138
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/PageTemplates.java100
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/QrsCache.java15
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfiles.java276
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfilesBuilder.java34
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRuleBuilder.java53
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRules.java52
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/FederationSearcher.java269
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/GenericTarget.java29
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProvider.java115
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProviderSearcher.java22
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/LocalProvider.java188
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Provider.java45
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChain.java31
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChains.java129
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Searcher.java26
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Source.java61
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroup.java95
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupRegistry.java58
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/LocalClustersCreator.java51
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/VespaSearchChainsCreator.java141
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java96
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilder.java58
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleMapper.java132
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java68
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java576
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java45
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocprocOptionsBuilder.java81
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocumentApiOptionsBuilder.java119
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/InconsistentSchemaAndCodeError.java7
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilder.java172
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/document/DocumentFactoryBuilder.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/container/xml/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/BucketSplitting.java60
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java121
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/Content.java347
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/ContentNode.java80
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearch.java44
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java299
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/DispatchSpec.java83
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/DistributionBitCalculator.java79
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/Distributor.java56
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java151
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/DocumentTypeVisitor.java62
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java117
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/PriorityMapping.java37
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/Redundancy.java57
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java55
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/SearchCoverage.java72
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java420
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/StorageNode.java116
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/TuningDispatch.java72
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java604
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DocumentSelectionBuilder.java80
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilder.java22
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomDispatchBuilder.java53
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java24
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilder.java27
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilder.java30
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/EngineFactoryBuilder.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java40
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/SearchDefinitionBuilder.java36
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/cluster/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/engines/DummyPersistence.java42
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/engines/PersistenceEngine.java41
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonEngine.java44
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonProvider.java18
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/engines/RPCEngine.java57
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/engines/VDSEngine.java83
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/engines/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java73
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/IntegrityCheckerProducer.java80
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/PersistenceProducer.java51
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java56
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorVisitorProducer.java59
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java149
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributionConfigProducer.java59
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributor.java120
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributorService.java98
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/filedistribution/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesBuilder.java33
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesModel.java28
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomModuleBuilder.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceBuilder.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceClusterBuilder.java44
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/builder/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/service/Module.java18
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/service/Service.java57
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/service/ServiceCluster.java58
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/generic/service/package-info.java5
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/package-info.java188
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/routing/DocumentProtocol.java385
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/routing/Protocol.java29
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/routing/Routing.java201
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/AbstractSearchCluster.java129
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/Dispatch.java226
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroup.java131
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroupBuilder.java110
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/DocumentDatabase.java90
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/DocumentSelectionConverter.java54
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/IndexedElasticSearchCluster.java45
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/IndexedSearchCluster.java390
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/IndexingDocprocChain.java44
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/IndexingProcessor.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/MultilevelDispatchValidator.java89
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/NodeSpec.java33
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/SearchCluster.java153
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/SearchColumn.java27
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinition.java46
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinitionXMLHandler.java32
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/SearchInterface.java16
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/SearchNode.java288
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/SearchNodeWrapper.java29
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/StreamingSearchCluster.java123
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/TransactionLogServer.java62
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/search/Tuning.java377
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/utils/Duration.java69
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/utils/FileSender.java149
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/utils/FreezableMap.java91
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/utils/internal/ReflectionUtil.java114
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/utils/package-info.java5
-rw-r--r--config-model/src/main/javacc/SDParser.jj2516
-rwxr-xr-xconfig-model/src/main/perl/deploy614
-rwxr-xr-xconfig-model/src/main/perl/deploy-application77
-rwxr-xr-xconfig-model/src/main/perl/expand-config.pl84
-rwxr-xr-xconfig-model/src/main/perl/vespa-replicate-log-stream37
-rw-r--r--config-model/src/main/resources/schema/.gitignore2
-rw-r--r--config-model/src/main/resources/schema/admin.rnc95
-rw-r--r--config-model/src/main/resources/schema/clients-v2.rnc64
-rw-r--r--config-model/src/main/resources/schema/clients.rnc6
-rw-r--r--config-model/src/main/resources/schema/common.rnc58
-rw-r--r--config-model/src/main/resources/schema/container-include.rnc8
-rw-r--r--config-model/src/main/resources/schema/container.rnc41
-rw-r--r--config-model/src/main/resources/schema/containercluster.rnc217
-rw-r--r--config-model/src/main/resources/schema/content.rnc254
-rw-r--r--config-model/src/main/resources/schema/docproc-standalone.rnc6
-rw-r--r--config-model/src/main/resources/schema/docproc.rnc98
-rw-r--r--config-model/src/main/resources/schema/federation.rnc73
-rw-r--r--config-model/src/main/resources/schema/genericcluster.rnc22
-rw-r--r--config-model/src/main/resources/schema/genericmodule.rnc8
-rw-r--r--config-model/src/main/resources/schema/hosts.rnc10
-rw-r--r--config-model/src/main/resources/schema/legacygenericcluster.rnc20
-rw-r--r--config-model/src/main/resources/schema/legacygenericmodule.rnc8
-rw-r--r--config-model/src/main/resources/schema/processing.rnc39
-rw-r--r--config-model/src/main/resources/schema/routing-standalone.rnc4
-rw-r--r--config-model/src/main/resources/schema/routing.rnc28
-rw-r--r--config-model/src/main/resources/schema/schemas.xml6
-rw-r--r--config-model/src/main/resources/schema/search.rnc234
-rw-r--r--config-model/src/main/resources/schema/searchchains-standalone.rnc4
-rw-r--r--config-model/src/main/resources/schema/searchchains.rnc74
-rw-r--r--config-model/src/main/resources/schema/services.rnc31
-rw-r--r--config-model/src/main/resources/schema/storage.rnc237
-rwxr-xr-xconfig-model/src/main/sh/validate-application87
-rw-r--r--config-model/src/test/cfg/admin/adminconfig20/hosts.xml9
-rw-r--r--config-model/src/test/cfg/admin/adminconfig20/services.xml14
-rw-r--r--config-model/src/test/cfg/admin/adminconfigbaseport/hosts.xml7
-rw-r--r--config-model/src/test/cfg/admin/adminconfigbaseport/services.xml17
-rw-r--r--config-model/src/test/cfg/admin/metricconfig/hosts.xml11
-rw-r--r--config-model/src/test/cfg/admin/metricconfig/searchdefinitions/music.sd16
-rw-r--r--config-model/src/test/cfg/admin/metricconfig/services.xml58
-rw-r--r--config-model/src/test/cfg/admin/multipleconfigservers/hosts.xml10
-rw-r--r--config-model/src/test/cfg/admin/multipleconfigservers/services.xml14
-rw-r--r--config-model/src/test/cfg/admin/sdconfigs/pan-rtx.cfg3
-rw-r--r--config-model/src/test/cfg/admin/sdconfigs/partitions.cfg2
-rw-r--r--config-model/src/test/cfg/admin/simpleadminconfig20/hosts.xml7
-rw-r--r--config-model/src/test/cfg/admin/simpleadminconfig20/services.xml9
-rw-r--r--config-model/src/test/cfg/admin/userconfigs/function-test.def73
-rw-r--r--config-model/src/test/cfg/admin/userconfigs/functiontest-defaultvalues.xml62
-rw-r--r--config-model/src/test/cfg/admin/userconfigs/statistics.cfg4
-rw-r--r--config-model/src/test/cfg/admin/userconfigs/whitespace-test.xml6
-rw-r--r--config-model/src/test/cfg/application/app1/components/defs-only.jarbin0 -> 986 bytes
-rw-r--r--config-model/src/test/cfg/application/app1/components/file.txt1
-rw-r--r--config-model/src/test/cfg/application/app1/files/foo.json1
-rw-r--r--config-model/src/test/cfg/application/app1/files/sub/bar.json1
-rw-r--r--config-model/src/test/cfg/application/app1/hosts.xml11
-rw-r--r--config-model/src/test/cfg/application/app1/searchdefinitions/bar.expression1
-rw-r--r--config-model/src/test/cfg/application/app1/searchdefinitions/foo.expression1
-rw-r--r--config-model/src/test/cfg/application/app1/searchdefinitions/laptop.sd41
-rw-r--r--config-model/src/test/cfg/application/app1/searchdefinitions/music.sd44
-rw-r--r--config-model/src/test/cfg/application/app1/searchdefinitions/pc.sd47
-rw-r--r--config-model/src/test/cfg/application/app1/searchdefinitions/product.sd13
-rw-r--r--config-model/src/test/cfg/application/app1/searchdefinitions/sock.sd27
-rw-r--r--config-model/src/test/cfg/application/app1/services.xml32
-rw-r--r--config-model/src/test/cfg/application/app_genericservices/hosts.xml21
-rw-r--r--config-model/src/test/cfg/application/app_genericservices/searchdefinitions/music.sd44
-rw-r--r--config-model/src/test/cfg/application/app_genericservices/services.xml59
-rw-r--r--config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mail.sd9
-rw-r--r--config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mailbox.sd9
-rw-r--r--config-model/src/test/cfg/application/app_nohosts/searchdefinitions/message.sd9
-rw-r--r--config-model/src/test/cfg/application/app_nohosts/services.xml71
-rw-r--r--config-model/src/test/cfg/application/app_permanent/permanent-services.xml7
-rw-r--r--config-model/src/test/cfg/application/app_qrserverandgw/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/app_qrserverandgw/searchdefinitions/message.sd9
-rw-r--r--config-model/src/test/cfg/application/app_qrserverandgw/services.xml29
-rw-r--r--config-model/src/test/cfg/application/app_sdbundles/components/testbundle.jarbin0 -> 1346 bytes
-rw-r--r--config-model/src/test/cfg/application/app_sdbundles/components/testbundle2.jarbin0 -> 681 bytes
-rw-r--r--config-model/src/test/cfg/application/app_sdbundles/files/foo.txt1
-rw-r--r--config-model/src/test/cfg/application/app_sdbundles/files/subdir/bar.txt1
-rw-r--r--config-model/src/test/cfg/application/app_sdbundles/hosts.xml11
-rw-r--r--config-model/src/test/cfg/application/app_sdbundles/services.xml32
-rw-r--r--config-model/src/test/cfg/application/classes/attributes.def7
-rw-r--r--config-model/src/test/cfg/application/com/yahoo/vespa/model/test/.gitignore1
-rw-r--r--config-model/src/test/cfg/application/components/com.yahoo.searcher1.jarbin0 -> 8413 bytes
-rw-r--r--config-model/src/test/cfg/application/configdeftest/configdefinitions/bar.def3
-rw-r--r--config-model/src/test/cfg/application/configdeftest/configdefinitions/baz.def3
-rw-r--r--config-model/src/test/cfg/application/configdeftest/configdefinitions/foo.def4
-rw-r--r--config-model/src/test/cfg/application/configdeftest/configdefinitions/qux.foo.def4
-rw-r--r--config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.bar.def4
-rw-r--r--config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.def4
-rw-r--r--config-model/src/test/cfg/application/configuredportconfig/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/configuredportconfig/services.xml20
-rw-r--r--config-model/src/test/cfg/application/custompropconfig/hosts.xml9
-rw-r--r--config-model/src/test/cfg/application/custompropconfig/services.xml7
-rw-r--r--config-model/src/test/cfg/application/doubleconfig/hosts.xml9
-rw-r--r--config-model/src/test/cfg/application/doubleconfig/services.xml14
-rw-r--r--config-model/src/test/cfg/application/include_dirs/dir1/default.xml6
-rw-r--r--config-model/src/test/cfg/application/include_dirs/dir2/chain2.xml8
-rw-r--r--config-model/src/test/cfg/application/include_dirs/dir2/chain3.xml10
-rw-r--r--config-model/src/test/cfg/application/include_dirs/empty_dir/.gitignore0
-rw-r--r--config-model/src/test/cfg/application/include_dirs/jdisc_dir/jdisc1.xml4
-rw-r--r--config-model/src/test/cfg/application/include_dirs/services.xml26
-rw-r--r--config-model/src/test/cfg/application/invalid_legacy_user_config/configs/qr-searchers.cfg5
-rw-r--r--config-model/src/test/cfg/application/invalid_legacy_user_config/services.xml10
-rw-r--r--config-model/src/test/cfg/application/metricsconfig/hosts.xml9
-rw-r--r--config-model/src/test/cfg/application/metricsconfig/services.xml22
-rw-r--r--config-model/src/test/cfg/application/newfilenames/hosts.xml9
-rw-r--r--config-model/src/test/cfg/application/newfilenames/services.xml9
-rw-r--r--config-model/src/test/cfg/application/plugins/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/plugins/services.xml22
-rw-r--r--config-model/src/test/cfg/application/sdfilenametest/searchdefinitions/notmusic.sd12
-rw-r--r--config-model/src/test/cfg/application/sdfilenametest/services.xml8
-rw-r--r--config-model/src/test/cfg/application/serverdefs/attributes.def8
-rw-r--r--config-model/src/test/cfg/application/simpleconfig/hosts.xml9
-rw-r--r--config-model/src/test/cfg/application/simpleconfig/services.xml14
-rw-r--r--config-model/src/test/cfg/application/treeconfig/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/treeconfig/services.xml21
-rw-r--r--config-model/src/test/cfg/application/validation/components/.gitignore0
-rw-r--r--config-model/src/test/cfg/application/validation/index_struct/searchdefinitions/simple.sd8
-rw-r--r--config-model/src/test/cfg/application/validation/index_struct/services.xml17
-rw-r--r--config-model/src/test/cfg/application/validation/invalidjar_app/components/invalid.jar0
-rw-r--r--config-model/src/test/cfg/application/validation/prefix/searchdefinitions/simple.sd9
-rw-r--r--config-model/src/test/cfg/application/validation/prefix/services.xml16
-rw-r--r--config-model/src/test/cfg/application/validation/prefix_index/searchdefinitions/simple.sd9
-rw-r--r--config-model/src/test/cfg/application/validation/prefix_index/services.xml16
-rw-r--r--config-model/src/test/cfg/application/validation/prefix_index_and_attribute/searchdefinitions/simple.sd9
-rw-r--r--config-model/src/test/cfg/application/validation/prefix_index_and_attribute/services.xml16
-rw-r--r--config-model/src/test/cfg/application/validation/prefix_streaming/searchdefinitions/simple.sd9
-rw-r--r--config-model/src/test/cfg/application/validation/prefix_streaming/services.xml16
-rw-r--r--config-model/src/test/cfg/application/validation/search_alltypes/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/validation/search_alltypes/searchdefinitions/simple.sd16
-rw-r--r--config-model/src/test/cfg/application/validation/search_alltypes/services.xml17
-rw-r--r--config-model/src/test/cfg/application/validation/search_content/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/validation/search_content/searchdefinitions/simple.sd7
-rw-r--r--config-model/src/test/cfg/application/validation/search_content/services.xml29
-rw-r--r--config-model/src/test/cfg/application/validation/search_empty_content/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/validation/search_empty_content/searchdefinitions/simple.sd6
-rw-r--r--config-model/src/test/cfg/application/validation/search_empty_content/services.xml17
-rw-r--r--config-model/src/test/cfg/application/validation/search_struct/hosts.xml7
-rw-r--r--config-model/src/test/cfg/application/validation/search_struct/searchdefinitions/simple.sd10
-rw-r--r--config-model/src/test/cfg/application/validation/search_struct/services.xml17
-rw-r--r--config-model/src/test/cfg/application/validation/testjars/missing_osgi_headers.jarbin0 -> 2542 bytes
-rw-r--r--config-model/src/test/cfg/application/validation/testjars/nomanifest.jarbin0 -> 2283 bytes
-rw-r--r--config-model/src/test/cfg/application/validation/testjars/ok.jarbin0 -> 2550 bytes
-rw-r--r--config-model/src/test/cfg/application/validation/testjars/snapshot_bundle.jarbin0 -> 1579 bytes
-rw-r--r--config-model/src/test/cfg/application/validation/testjars/test.jarbin0 -> 2578 bytes
-rw-r--r--config-model/src/test/cfg/application/validation/testjars/wrong_classpath.jarbin0 -> 2574 bytes
-rw-r--r--config-model/src/test/cfg/application/validation/testjars/wrong_export.jarbin0 -> 2578 bytes
-rw-r--r--config-model/src/test/cfg/clients/advancedconfig.v2/hosts.xml7
-rw-r--r--config-model/src/test/cfg/clients/advancedconfig.v2/searchdefinitions/music.sd13
-rw-r--r--config-model/src/test/cfg/clients/advancedconfig.v2/services.xml70
-rw-r--r--config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/hosts.xml7
-rw-r--r--config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/searchdefinitions/music.sd13
-rw-r--r--config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/services.xml63
-rw-r--r--config-model/src/test/cfg/clients/simpleconfig.v2/searchdefinitions/.gitignore0
-rw-r--r--config-model/src/test/cfg/container/data/configserverinclude/hosted-vespa/hosted.xml10
-rw-r--r--config-model/src/test/cfg/container/data/configserverinclude/services.xml10
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude/docprocinclude1/foo/bar/docprocinclude1.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude/hosts.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude/processinginclude1/processinginclude1.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch1.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch2.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude/searchinclude2/includedsearch3.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude/services.xml35
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude2/hosts.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude2/services.xml19
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude3/hosts.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude3/services.xml19
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude4/hosts.xml7
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude4/services.xml19
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude5/searchinclude/processing.xml6
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude5/services.xml18
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude6/empty_dir/.gitignore0
-rw-r--r--config-model/src/test/cfg/container/data/containerinclude6/services.xml18
-rw-r--r--config-model/src/test/cfg/container/data/include_xml_error/dir1/default.xml6
-rw-r--r--config-model/src/test/cfg/container/data/include_xml_error/services.xml20
-rwxr-xr-xconfig-model/src/test/cfg/routing/content_two_clusters/documentrouteselectorpolicy.cfg6
-rw-r--r--config-model/src/test/cfg/routing/content_two_clusters/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/content_two_clusters/messagebus.cfg44
-rw-r--r--config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/mobile.sd15
-rw-r--r--config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/music.sd13
-rw-r--r--config-model/src/test/cfg/routing/content_two_clusters/services.xml31
-rwxr-xr-xconfig-model/src/test/cfg/routing/contentsimpleconfig/documentrouteselectorpolicy.cfg3
-rw-r--r--config-model/src/test/cfg/routing/contentsimpleconfig/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/contentsimpleconfig/messagebus.cfg25
-rw-r--r--config-model/src/test/cfg/routing/contentsimpleconfig/searchdefinitions/music.sd13
-rw-r--r--config-model/src/test/cfg/routing/contentsimpleconfig/services.xml21
-rwxr-xr-xconfig-model/src/test/cfg/routing/defaultconfig/documentrouteselectorpolicy.cfg1
-rwxr-xr-xconfig-model/src/test/cfg/routing/defaultconfig/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/defaultconfig/messagebus.cfg6
-rwxr-xr-xconfig-model/src/test/cfg/routing/defaultconfig/services.xml13
-rwxr-xr-xconfig-model/src/test/cfg/routing/duplicatehop/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/duplicatehop/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/duplicatehop/services.xml18
-rwxr-xr-xconfig-model/src/test/cfg/routing/duplicateroute/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/duplicateroute/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/duplicateroute/services.xml18
-rwxr-xr-xconfig-model/src/test/cfg/routing/emptyhop/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/emptyhop/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/emptyhop/services.xml13
-rwxr-xr-xconfig-model/src/test/cfg/routing/emptyroute/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/emptyroute/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/emptyroute/services.xml13
-rwxr-xr-xconfig-model/src/test/cfg/routing/hopconfig/documentrouteselectorpolicy.cfg1
-rwxr-xr-xconfig-model/src/test/cfg/routing/hopconfig/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/hopconfig/messagebus.cfg15
-rwxr-xr-xconfig-model/src/test/cfg/routing/hopconfig/services.xml24
-rwxr-xr-xconfig-model/src/test/cfg/routing/hoperror/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/hoperror/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/hoperror/services.xml13
-rwxr-xr-xconfig-model/src/test/cfg/routing/hoperrorinrecipient/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/hoperrorinrecipient/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/hoperrorinrecipient/services.xml15
-rwxr-xr-xconfig-model/src/test/cfg/routing/hoperrorinroute/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/hoperrorinroute/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/hoperrorinroute/services.xml13
-rwxr-xr-xconfig-model/src/test/cfg/routing/hopnotfound/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/hopnotfound/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/hopnotfound/services.xml13
-rw-r--r--config-model/src/test/cfg/routing/invalidstoragepolicy/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/invalidstoragepolicy/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/invalidstoragepolicy/services.xml11
-rwxr-xr-xconfig-model/src/test/cfg/routing/mismatchedrecipient/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/mismatchedrecipient/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/mismatchedrecipient/services.xml15
-rwxr-xr-xconfig-model/src/test/cfg/routing/replacehop/documentrouteselectorpolicy.cfg3
-rwxr-xr-xconfig-model/src/test/cfg/routing/replacehop/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/replacehop/messagebus.cfg22
-rwxr-xr-xconfig-model/src/test/cfg/routing/replacehop/searchdefinitions/music.sd13
-rwxr-xr-xconfig-model/src/test/cfg/routing/replacehop/services.xml32
-rwxr-xr-xconfig-model/src/test/cfg/routing/replaceroute/documentrouteselectorpolicy.cfg3
-rwxr-xr-xconfig-model/src/test/cfg/routing/replaceroute/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/replaceroute/messagebus.cfg19
-rwxr-xr-xconfig-model/src/test/cfg/routing/replaceroute/searchdefinitions/music.sd13
-rwxr-xr-xconfig-model/src/test/cfg/routing/replaceroute/services.xml28
-rwxr-xr-xconfig-model/src/test/cfg/routing/routeconfig/documentrouteselectorpolicy.cfg1
-rwxr-xr-xconfig-model/src/test/cfg/routing/routeconfig/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/routing/routeconfig/messagebus.cfg7
-rwxr-xr-xconfig-model/src/test/cfg/routing/routeconfig/services.xml14
-rwxr-xr-xconfig-model/src/test/cfg/routing/routenotfound/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/routenotfound/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/routenotfound/services.xml13
-rwxr-xr-xconfig-model/src/test/cfg/routing/routenotfoundinroute/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/routenotfoundinroute/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/routenotfoundinroute/services.xml13
-rwxr-xr-xconfig-model/src/test/cfg/routing/servicenotfound/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/servicenotfound/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/servicenotfound/services.xml15
-rwxr-xr-xconfig-model/src/test/cfg/routing/unexpectedrecipient/errors.txt1
-rw-r--r--config-model/src/test/cfg/routing/unexpectedrecipient/hosts.xml7
-rw-r--r--config-model/src/test/cfg/routing/unexpectedrecipient/services.xml20
-rw-r--r--config-model/src/test/cfg/search/compare/complex/hosts/dev-mathiasm/sentinel/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/hosts/zarya/sentinel/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r1/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r1/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r1/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r1/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/1/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg23
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg66
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/fdispatchrc.MODEL.cfg23
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/partitions.MODEL.cfg66
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r1/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r1/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r1/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r1/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx-rtlogic.MODEL.cfg19
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx-rtlogic.MODEL.cfg19
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/fdispatchrc.MODEL.cfg23
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/partitions.MODEL.cfg66
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.1/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.2/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/hosts/zarya/sentinel/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/cluster.music/c0/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/cluster.music/g0/c0/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg23
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg48
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.1/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.2/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/hosts/zarya/sentinel/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/cluster.music/c0/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/cluster.music/g0/c0/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx-rtlogic.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg23
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg48
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/cluster.streaming/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/search/qrservers/qrserver.0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/client/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/distributor/0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/fleetcontroller/0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/gateway/0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/storage/0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/hosts/zarya/sentinel/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/c0/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/g0/c0/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx-rtlogic.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/fdispatchrc.MODEL.cfg23
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/partitions.MODEL.cfg48
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r0/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r1/translogserver.MODEL.cfg7
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r1/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/rtx/0/pan-rtx.MODEL.cfg6
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/fdispatchrc.MODEL.cfg23
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/partitions.MODEL.cfg54
-rw-r--r--config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/qrservers/qrserver.0/.gitignore0
-rw-r--r--config-model/src/test/cfg/search/data/nextgen-simple-v2/searchdefinitions/nextgendoc.sd8
-rw-r--r--config-model/src/test/cfg/search/data/nextgen-simple-v2/services.xml37
-rw-r--r--config-model/src/test/cfg/search/data/onlybundles/components/testbundle.jarbin0 -> 696 bytes
-rw-r--r--config-model/src/test/cfg/search/data/onlybundles/services.xml34
-rw-r--r--config-model/src/test/cfg/search/data/travel/searchdefinitions/TTData.sd10
-rw-r--r--config-model/src/test/cfg/search/data/travel/searchdefinitions/TTEdge.sd10
-rw-r--r--config-model/src/test/cfg/search/data/travel/searchdefinitions/TTPOI.sd19
-rw-r--r--config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/base.sd8
-rw-r--r--config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/left.sd14
-rw-r--r--config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/music.sd15
-rw-r--r--config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/right.sd8
-rw-r--r--config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/services.xml17
-rw-r--r--config-model/src/test/cfg/search/data/v2/modularsearchchains/hosts.xml7
-rw-r--r--config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain2.xml9
-rw-r--r--config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain3.xml10
-rw-r--r--config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/default.xml6
-rw-r--r--config-model/src/test/cfg/search/data/v2/modularsearchchains/services.xml25
-rw-r--r--config-model/src/test/cfg/search/data/v2/onlybundles/components/testbundle.jarbin0 -> 696 bytes
-rw-r--r--config-model/src/test/cfg/search/data/v2/onlybundles/services.xml34
-rw-r--r--config-model/src/test/cfg/search/data/v2/proton-yamas/hosts.xml7
-rw-r--r--config-model/src/test/cfg/search/data/v2/proton-yamas/searchdefinitions/music.sd13
-rw-r--r--config-model/src/test/cfg/search/data/v2/proton-yamas/services.xml30
-rw-r--r--config-model/src/test/cfg/search/data/v2/stripped/services.xml32
-rw-r--r--config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/hosts.xml7
-rwxr-xr-xconfig-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/searchdefinitions/music.sd13
-rw-r--r--config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/services.xml55
-rw-r--r--config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/hosts.xml11
-rw-r--r--config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/searchdefinitions/music.sd15
-rw-r--r--config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/services.xml30
-rw-r--r--config-model/src/test/cfg/storage/clustercontroller_advanced/hosts.xml15
-rw-r--r--config-model/src/test/cfg/storage/clustercontroller_advanced/searchdefinitions/music.sd15
-rw-r--r--config-model/src/test/cfg/storage/clustercontroller_advanced/services.xml37
-rw-r--r--config-model/src/test/configmodel/types/documentmanager.cfg221
-rw-r--r--config-model/src/test/configmodel/types/documenttypes.cfg599
-rw-r--r--config-model/src/test/configmodel/types/documenttypes_with_doc_field.cfg109
-rw-r--r--config-model/src/test/configmodel/types/other_doc.sd3
-rw-r--r--config-model/src/test/configmodel/types/type_with_doc_field.sd10
-rw-r--r--config-model/src/test/configmodel/types/types.sd151
-rw-r--r--config-model/src/test/derived/advanced/advanced.sd105
-rw-r--r--config-model/src/test/derived/advanced/attributes.cfg19
-rw-r--r--config-model/src/test/derived/advanced/documentmanager.cfg84
-rw-r--r--config-model/src/test/derived/advanced/ilscripts.cfg24
-rw-r--r--config-model/src/test/derived/advanced/index-info.cfg97
-rw-r--r--config-model/src/test/derived/advanced/rank-profiles.cfg18
-rw-r--r--config-model/src/test/derived/advanced/summary.cfg33
-rw-r--r--config-model/src/test/derived/advanced/summarymap.cfg16
-rwxr-xr-xconfig-model/src/test/derived/annotationsimplicitstruct/annotationsimplicitstruct.sd12
-rwxr-xr-xconfig-model/src/test/derived/annotationsimplicitstruct/documentmanager.cfg49
-rw-r--r--config-model/src/test/derived/annotationsimplicitstruct/ilscripts.cfg2
-rwxr-xr-xconfig-model/src/test/derived/annotationsimplicitstruct/index-info.cfg9
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance/annotationsinheritance.sd39
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance/documentmanager.cfg135
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance/ilscripts.cfg2
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance/index-info.cfg9
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance2/annotationsinheritance2.sd32
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance2/documentmanager.cfg97
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance2/ilscripts.cfg2
-rwxr-xr-xconfig-model/src/test/derived/annotationsinheritance2/index-info.cfg9
-rw-r--r--config-model/src/test/derived/annotationsoutsideofdocument/annotationsoutsideofdocument.sd11
-rw-r--r--config-model/src/test/derived/annotationspolymorphy/annotationspolymorphy.sd15
-rwxr-xr-xconfig-model/src/test/derived/annotationspolymorphy/documentmanager.cfg58
-rwxr-xr-xconfig-model/src/test/derived/annotationspolymorphy/index-info.cfg9
-rwxr-xr-xconfig-model/src/test/derived/annotationsreference/annotationsreference.sd26
-rwxr-xr-xconfig-model/src/test/derived/annotationsreference/documentmanager.cfg94
-rwxr-xr-xconfig-model/src/test/derived/annotationsreference/ilscripts.cfg2
-rwxr-xr-xconfig-model/src/test/derived/annotationsreference/index-info.cfg9
-rw-r--r--config-model/src/test/derived/annotationsreference2/annotationsreference2.sd9
-rwxr-xr-xconfig-model/src/test/derived/annotationssimple/annotationssimple.sd8
-rwxr-xr-xconfig-model/src/test/derived/annotationssimple/documentmanager.cfg40
-rw-r--r--config-model/src/test/derived/annotationssimple/ilscripts.cfg2
-rwxr-xr-xconfig-model/src/test/derived/annotationssimple/index-info.cfg9
-rw-r--r--config-model/src/test/derived/annotationsstruct/annotationsstruct.sd11
-rw-r--r--config-model/src/test/derived/annotationsstruct/documentmanager.cfg58
-rw-r--r--config-model/src/test/derived/annotationsstructarray/annotationsstructarray.sd11
-rw-r--r--config-model/src/test/derived/annotationsstructarray/documentmanager.cfg60
-rw-r--r--config-model/src/test/derived/arrays/arrays.sd32
-rw-r--r--config-model/src/test/derived/arrays/documentmanager.cfg63
-rw-r--r--config-model/src/test/derived/arrays/ilscripts.cfg12
-rw-r--r--config-model/src/test/derived/arrays/index-info.cfg63
-rw-r--r--config-model/src/test/derived/attributeprefetch/attributeprefetch.sd86
-rw-r--r--config-model/src/test/derived/attributeprefetch/attributes.cfg342
-rw-r--r--config-model/src/test/derived/attributeprefetch/documentmanager.cfg127
-rw-r--r--config-model/src/test/derived/attributeprefetch/ilscripts.cfg38
-rw-r--r--config-model/src/test/derived/attributeprefetch/index-info.cfg115
-rw-r--r--config-model/src/test/derived/attributeprefetch/rank-profiles.cfg10
-rw-r--r--config-model/src/test/derived/attributeprefetch/summary.cfg51
-rw-r--r--config-model/src/test/derived/attributeprefetch/summarymap.cfg61
-rw-r--r--config-model/src/test/derived/attributerank/attributerank.sd41
-rw-r--r--config-model/src/test/derived/attributerank/rank-profiles.cfg34
-rw-r--r--config-model/src/test/derived/attributes/attributes.cfg342
-rw-r--r--config-model/src/test/derived/attributes/attributes.sd121
-rw-r--r--config-model/src/test/derived/attributes/ilscripts.cfg42
-rw-r--r--config-model/src/test/derived/attributes/index-info.cfg141
-rw-r--r--config-model/src/test/derived/attributes/summarymap.cfg52
-rw-r--r--config-model/src/test/derived/combinedattributeandindexsearch/combinedattributeandindexsearch.sd34
-rw-r--r--config-model/src/test/derived/combinedattributeandindexsearch/index-info.cfg41
-rw-r--r--config-model/src/test/derived/complex/attributes.cfg171
-rw-r--r--config-model/src/test/derived/complex/complex.sd150
-rw-r--r--config-model/src/test/derived/complex/documentmanager.cfg119
-rw-r--r--config-model/src/test/derived/complex/ilscripts.cfg48
-rw-r--r--config-model/src/test/derived/complex/rank-profiles.cfg68
-rw-r--r--config-model/src/test/derived/complex/summary.cfg43
-rw-r--r--config-model/src/test/derived/complex/summarymap.cfg34
-rw-r--r--config-model/src/test/derived/deriver/child.sd12
-rw-r--r--config-model/src/test/derived/deriver/grandparent.sd12
-rw-r--r--config-model/src/test/derived/deriver/ilscripts.cfg8
-rw-r--r--config-model/src/test/derived/deriver/parent.sd12
-rw-r--r--config-model/src/test/derived/documentderiver/compression_body.sd20
-rw-r--r--config-model/src/test/derived/documentderiver/compression_both.sd26
-rw-r--r--config-model/src/test/derived/documentderiver/compression_header.sd20
-rw-r--r--config-model/src/test/derived/documentderiver/documentmanager.cfg319
-rw-r--r--config-model/src/test/derived/documentderiver/mail.sd112
-rw-r--r--config-model/src/test/derived/documentderiver/music.sd44
-rw-r--r--config-model/src/test/derived/documentderiver/newsarticle.sd126
-rw-r--r--config-model/src/test/derived/documentderiver/newssummary.sd165
-rw-r--r--config-model/src/test/derived/documentderiver/sombrero.sd36
-rw-r--r--config-model/src/test/derived/documentderiver/vsmfields.cfg489
-rw-r--r--config-model/src/test/derived/documentderiver/vsmsummary.cfg6
-rw-r--r--config-model/src/test/derived/emptychild/child.sd5
-rw-r--r--config-model/src/test/derived/emptychild/parent.sd8
-rw-r--r--config-model/src/test/derived/emptychild/summary.cfg19
-rw-r--r--config-model/src/test/derived/emptydefault/attributes.cfg0
-rw-r--r--config-model/src/test/derived/emptydefault/documentmanager.cfg43
-rw-r--r--config-model/src/test/derived/emptydefault/emptydefault.sd16
-rw-r--r--config-model/src/test/derived/emptydefault/ilscripts.cfg6
-rw-r--r--config-model/src/test/derived/emptydefault/index-info.cfg25
-rw-r--r--config-model/src/test/derived/emptydefault/rank-profiles.cfg10
-rw-r--r--config-model/src/test/derived/emptydefault/summary.cfg9
-rw-r--r--config-model/src/test/derived/emptydefault/summarymap.cfg7
-rw-r--r--config-model/src/test/derived/exactmatch/exactmatch.sd21
-rw-r--r--config-model/src/test/derived/exactmatch/ilscripts.cfg6
-rw-r--r--config-model/src/test/derived/exactmatch/index-info.cfg21
-rw-r--r--config-model/src/test/derived/fieldlength/attributes.cfg4
-rw-r--r--config-model/src/test/derived/fieldlength/fieldlength.sd73
-rwxr-xr-xconfig-model/src/test/derived/flickr/flickrphotos.sd24
-rw-r--r--config-model/src/test/derived/gemini2/gemini.sd27
-rw-r--r--config-model/src/test/derived/gemini2/rank-profiles.cfg29
-rw-r--r--config-model/src/test/derived/id/attributes.cfg0
-rw-r--r--config-model/src/test/derived/id/documentmanager.cfg40
-rw-r--r--config-model/src/test/derived/id/id.sd12
-rw-r--r--config-model/src/test/derived/id/ilscripts.cfg4
-rw-r--r--config-model/src/test/derived/id/index-info.cfg45
-rw-r--r--config-model/src/test/derived/id/rank-profiles.cfg10
-rw-r--r--config-model/src/test/derived/id/summary.cfg11
-rw-r--r--config-model/src/test/derived/id/summarymap.cfg7
-rw-r--r--config-model/src/test/derived/id/vsmsummary.cfg8
-rw-r--r--config-model/src/test/derived/indexinfo_fieldsets/index-info.cfg74
-rw-r--r--config-model/src/test/derived/indexinfo_fieldsets/indexinfo_fieldsets.sd50
-rw-r--r--config-model/src/test/derived/indexinfo_lowercase/index-info.cfg249
-rw-r--r--config-model/src/test/derived/indexinfo_lowercase/indexinfo_lowercase.sd120
-rw-r--r--config-model/src/test/derived/indexschema/index-info.cfg311
-rw-r--r--config-model/src/test/derived/indexschema/indexschema.cfg221
-rw-r--r--config-model/src/test/derived/indexschema/indexschema.sd161
-rw-r--r--config-model/src/test/derived/indexschema/vsmfields.cfg194
-rw-r--r--config-model/src/test/derived/indexswitches/attributes.cfg0
-rw-r--r--config-model/src/test/derived/indexswitches/documentmanager.cfg50
-rw-r--r--config-model/src/test/derived/indexswitches/ilscripts.cfg9
-rw-r--r--config-model/src/test/derived/indexswitches/index-info.cfg41
-rw-r--r--config-model/src/test/derived/indexswitches/indexswitches.sd32
-rw-r--r--config-model/src/test/derived/indexswitches/rank-profiles.cfg10
-rw-r--r--config-model/src/test/derived/indexswitches/summary.cfg15
-rw-r--r--config-model/src/test/derived/indexswitches/summarymap.cfg7
-rw-r--r--config-model/src/test/derived/inheritance/attributes.cfg10
-rw-r--r--config-model/src/test/derived/inheritance/child.sd16
-rw-r--r--config-model/src/test/derived/inheritance/documentmanager.cfg426
-rw-r--r--config-model/src/test/derived/inheritance/father.sd12
-rw-r--r--config-model/src/test/derived/inheritance/grandparent.sd12
-rw-r--r--config-model/src/test/derived/inheritance/ilscripts.cfg9
-rw-r--r--config-model/src/test/derived/inheritance/index-info.cfg30
-rw-r--r--config-model/src/test/derived/inheritance/mother.sd13
-rw-r--r--config-model/src/test/derived/inheritance/mother/documentmanager.cfg426
-rw-r--r--config-model/src/test/derived/inheritance/rank-profiles.cfg16
-rw-r--r--config-model/src/test/derived/inheritance/summary.cfg26
-rw-r--r--config-model/src/test/derived/inheritance/summarymap.cfg17
-rw-r--r--config-model/src/test/derived/inheritancebadtypes/child.sd8
-rw-r--r--config-model/src/test/derived/inheritancebadtypes/parent.sd8
-rw-r--r--config-model/src/test/derived/inheritdiamond/.gitignore1
-rw-r--r--config-model/src/test/derived/inheritdiamond/child.sd12
-rw-r--r--config-model/src/test/derived/inheritdiamond/documentmanager.cfg269
-rw-r--r--config-model/src/test/derived/inheritdiamond/father.sd8
-rw-r--r--config-model/src/test/derived/inheritdiamond/grandparent.sd8
-rw-r--r--config-model/src/test/derived/inheritdiamond/mother.sd8
-rw-r--r--config-model/src/test/derived/inheritfromgrandparent/child.sd6
-rw-r--r--config-model/src/test/derived/inheritfromgrandparent/documentmanager.cfg99
-rw-r--r--config-model/src/test/derived/inheritfromgrandparent/grandparent.sd8
-rw-r--r--config-model/src/test/derived/inheritfromgrandparent/parent.sd6
-rw-r--r--config-model/src/test/derived/inheritfromnull/inheritfromnull.sd5
-rw-r--r--config-model/src/test/derived/inheritfromparent/attributes.cfg19
-rw-r--r--config-model/src/test/derived/inheritfromparent/child.sd6
-rw-r--r--config-model/src/test/derived/inheritfromparent/documentmanager.cfg81
-rw-r--r--config-model/src/test/derived/inheritfromparent/documenttypes.cfg119
-rw-r--r--config-model/src/test/derived/inheritfromparent/parent.sd17
-rw-r--r--config-model/src/test/derived/inheritfromparent/summarymap.cfg10
-rw-r--r--config-model/src/test/derived/inheritstruct/child.sd10
-rw-r--r--config-model/src/test/derived/inheritstruct/index-info.cfg21
-rw-r--r--config-model/src/test/derived/inheritstruct/parent.sd8
-rw-r--r--config-model/src/test/derived/integerattributetostringindex/integerattributetostringindex.sd28
-rw-r--r--config-model/src/test/derived/integerattributetostringindex/summary.cfg29
-rw-r--r--config-model/src/test/derived/mail/attributes.cfg40
-rw-r--r--config-model/src/test/derived/mail/documentmanager.cfg115
-rw-r--r--config-model/src/test/derived/mail/ilscripts.cfg33
-rw-r--r--config-model/src/test/derived/mail/index-info.cfg155
-rw-r--r--config-model/src/test/derived/mail/mail.sd99
-rw-r--r--config-model/src/test/derived/mail/onlydoc/documentmanager.cfg67
-rw-r--r--config-model/src/test/derived/mail/rank-profiles.cfg10
-rw-r--r--config-model/src/test/derived/mail/summary.cfg51
-rw-r--r--config-model/src/test/derived/mail/summarymap.cfg16
-rw-r--r--config-model/src/test/derived/mail/vsmfields.cfg108
-rw-r--r--config-model/src/test/derived/mail/vsmsummary.cfg38
-rw-r--r--config-model/src/test/derived/mlr/mlr.sd20
-rw-r--r--config-model/src/test/derived/mlr/summary.cfg23
-rw-r--r--config-model/src/test/derived/multiplesummaries/attributes.cfg16
-rw-r--r--config-model/src/test/derived/multiplesummaries/ilscripts.cfg15
-rw-r--r--config-model/src/test/derived/multiplesummaries/index-info.cfg62
-rwxr-xr-xconfig-model/src/test/derived/multiplesummaries/juniperrc.cfg17
-rw-r--r--config-model/src/test/derived/multiplesummaries/multiplesummaries.sd199
-rw-r--r--config-model/src/test/derived/multiplesummaries/summary.cfg186
-rw-r--r--config-model/src/test/derived/multiplesummaries/summarymap.cfg50
-rw-r--r--config-model/src/test/derived/music/attributes.cfg209
-rw-r--r--config-model/src/test/derived/music/defs/attributes.def7
-rw-r--r--config-model/src/test/derived/music/defs/documentmanager.def12
-rw-r--r--config-model/src/test/derived/music/defs/extra.def4
-rw-r--r--config-model/src/test/derived/music/defs/ilscripts.def5
-rw-r--r--config-model/src/test/derived/music/defs/rank-profiles.def344
-rw-r--r--config-model/src/test/derived/music/defs/summarymap.def15
-rw-r--r--config-model/src/test/derived/music/ilscripts.cfg76
-rw-r--r--config-model/src/test/derived/music/index-info.cfg215
-rwxr-xr-xconfig-model/src/test/derived/music/juniperrc.cfg21
-rw-r--r--config-model/src/test/derived/music/music.sd176
-rw-r--r--config-model/src/test/derived/music/rank-profiles.cfg34
-rw-r--r--config-model/src/test/derived/music/summary.cfg103
-rw-r--r--config-model/src/test/derived/music/summarymap.cfg46
-rw-r--r--config-model/src/test/derived/music3/music3.sd70
-rw-r--r--config-model/src/test/derived/newrank/attributes.cfg190
-rw-r--r--config-model/src/test/derived/newrank/defs/attributes.def7
-rw-r--r--config-model/src/test/derived/newrank/defs/documentmanager.def12
-rw-r--r--config-model/src/test/derived/newrank/defs/extra.def4
-rw-r--r--config-model/src/test/derived/newrank/defs/ilscripts.def5
-rw-r--r--config-model/src/test/derived/newrank/defs/rank-profiles.def344
-rw-r--r--config-model/src/test/derived/newrank/defs/summarymap.def14
-rw-r--r--config-model/src/test/derived/newrank/ilscripts.cfg66
-rw-r--r--config-model/src/test/derived/newrank/index-info.cfg181
-rwxr-xr-xconfig-model/src/test/derived/newrank/juniperrc.cfg21
-rw-r--r--config-model/src/test/derived/newrank/newrank.sd146
-rw-r--r--config-model/src/test/derived/newrank/rank-profiles.cfg10
-rw-r--r--config-model/src/test/derived/newrank/summary.cfg97
-rw-r--r--config-model/src/test/derived/newrank/summarymap.cfg49
-rwxr-xr-xconfig-model/src/test/derived/orderilscripts/ilscripts.cfg5
-rwxr-xr-xconfig-model/src/test/derived/orderilscripts/orderilscripts.sd13
-rw-r--r--config-model/src/test/derived/position_array/ilscripts.cfg4
-rw-r--r--config-model/src/test/derived/position_array/index-info.cfg39
-rw-r--r--config-model/src/test/derived/position_array/position_array.sd8
-rw-r--r--config-model/src/test/derived/position_attribute/ilscripts.cfg4
-rw-r--r--config-model/src/test/derived/position_attribute/index-info.cfg37
-rw-r--r--config-model/src/test/derived/position_attribute/position_attribute.sd8
-rw-r--r--config-model/src/test/derived/position_extra/ilscripts.cfg4
-rw-r--r--config-model/src/test/derived/position_extra/index-info.cfg31
-rw-r--r--config-model/src/test/derived/position_extra/position_extra.sd11
-rw-r--r--config-model/src/test/derived/position_nosummary/position_nosummary.sd8
-rw-r--r--config-model/src/test/derived/position_nosummary/summary.cfg21
-rw-r--r--config-model/src/test/derived/position_nosummary/summarymap.cfg16
-rw-r--r--config-model/src/test/derived/position_summary/position_summary.sd8
-rw-r--r--config-model/src/test/derived/position_summary/summary.cfg23
-rw-r--r--config-model/src/test/derived/position_summary/summarymap.cfg19
-rw-r--r--config-model/src/test/derived/position_summary/vsmsummary.cfg14
-rw-r--r--config-model/src/test/derived/predicate_attribute/attributes.cfg19
-rw-r--r--config-model/src/test/derived/predicate_attribute/index-info.cfg15
-rw-r--r--config-model/src/test/derived/predicate_attribute/predicate_attribute.sd14
-rw-r--r--config-model/src/test/derived/predicate_attribute/summary.cfg17
-rw-r--r--config-model/src/test/derived/predicate_attribute/summarymap.cfg7
-rw-r--r--config-model/src/test/derived/prefixexactattribute/attributes.cfg38
-rw-r--r--config-model/src/test/derived/prefixexactattribute/documentmanager.cfg52
-rw-r--r--config-model/src/test/derived/prefixexactattribute/ilscripts.cfg12
-rw-r--r--config-model/src/test/derived/prefixexactattribute/index-info.cfg41
-rw-r--r--config-model/src/test/derived/prefixexactattribute/prefixexactattribute.sd52
-rw-r--r--config-model/src/test/derived/prefixexactattribute/vsmfields.cfg38
-rw-r--r--config-model/src/test/derived/rankexpression/macro.expression1
-rw-r--r--config-model/src/test/derived/rankexpression/overflow.expression300
-rw-r--r--config-model/src/test/derived/rankexpression/rank-profiles.cfg296
-rw-r--r--config-model/src/test/derived/rankexpression/rankexpression.expression1
-rw-r--r--config-model/src/test/derived/rankexpression/rankexpression.sd297
-rw-r--r--config-model/src/test/derived/rankexpression/summary.cfg25
-rw-r--r--config-model/src/test/derived/rankexpression/summarymap.cfg10
-rw-r--r--config-model/src/test/derived/rankprofiles/rank-profiles.cfg98
-rw-r--r--config-model/src/test/derived/rankprofiles/rankprofiles.sd72
-rw-r--r--config-model/src/test/derived/rankproperties/rank-profiles.cfg48
-rw-r--r--config-model/src/test/derived/rankproperties/rankproperties.sd61
-rw-r--r--config-model/src/test/derived/ranktypes/attributes.cfg0
-rw-r--r--config-model/src/test/derived/ranktypes/documentmanager.cfg51
-rw-r--r--config-model/src/test/derived/ranktypes/ilscripts.cfg11
-rw-r--r--config-model/src/test/derived/ranktypes/index-info.cfg47
-rw-r--r--config-model/src/test/derived/ranktypes/rank-profiles.cfg79
-rw-r--r--config-model/src/test/derived/ranktypes/ranktypes.sd32
-rw-r--r--config-model/src/test/derived/ranktypes/summary.cfg13
-rw-r--r--config-model/src/test/derived/ranktypes/summarymap.cfg7
-rw-r--r--config-model/src/test/derived/reserved_position/reserved_position.sd4
-rw-r--r--config-model/src/test/derived/sorting/attributes.cfg57
-rw-r--r--config-model/src/test/derived/sorting/sorting.sd53
-rw-r--r--config-model/src/test/derived/streamingjuniper/streamingjuniper.sd15
-rw-r--r--config-model/src/test/derived/streamingjuniper/vsmsummary.cfg11
-rw-r--r--config-model/src/test/derived/streamingstruct/documentmanager.cfg127
-rw-r--r--config-model/src/test/derived/streamingstruct/onlydoc/documentmanager.cfg100
-rw-r--r--config-model/src/test/derived/streamingstruct/streamingstruct.sd184
-rw-r--r--config-model/src/test/derived/streamingstruct/summary.cfg51
-rw-r--r--config-model/src/test/derived/streamingstruct/summarymap.cfg13
-rw-r--r--config-model/src/test/derived/streamingstruct/vsmfields.cfg434
-rw-r--r--config-model/src/test/derived/streamingstruct/vsmsummary.cfg83
-rw-r--r--config-model/src/test/derived/streamingstructdefault/streamingstructdefault.sd24
-rw-r--r--config-model/src/test/derived/streamingstructdefault/summary.cfg15
-rwxr-xr-xconfig-model/src/test/derived/structanyorder/documentmanager.cfg83
-rw-r--r--config-model/src/test/derived/structanyorder/ilscripts.cfg6
-rwxr-xr-xconfig-model/src/test/derived/structanyorder/index-info.cfg247
-rwxr-xr-xconfig-model/src/test/derived/structanyorder/structanyorder.sd27
-rw-r--r--config-model/src/test/derived/tensor/attributes.cfg57
-rw-r--r--config-model/src/test/derived/tensor/documenttypes.cfg65
-rw-r--r--config-model/src/test/derived/tensor/summary.cfg25
-rw-r--r--config-model/src/test/derived/tensor/tensor.sd18
-rw-r--r--config-model/src/test/derived/twostreamingstructs/documentmanager.cfg166
-rw-r--r--config-model/src/test/derived/twostreamingstructs/streamingstruct.sd183
-rw-r--r--config-model/src/test/derived/twostreamingstructs/summary.cfg51
-rw-r--r--config-model/src/test/derived/twostreamingstructs/summarymap.cfg13
-rw-r--r--config-model/src/test/derived/twostreamingstructs/vsmfields.cfg322
-rw-r--r--config-model/src/test/derived/twostreamingstructs/vsmsummary.cfg83
-rw-r--r--config-model/src/test/derived/twostreamingstructs/whatever.sd16
-rw-r--r--config-model/src/test/derived/types/attributes.cfg209
-rw-r--r--config-model/src/test/derived/types/documentmanager.cfg218
-rw-r--r--config-model/src/test/derived/types/ilscripts.cfg55
-rw-r--r--config-model/src/test/derived/types/index-info.cfg427
-rw-r--r--config-model/src/test/derived/types/rank-profiles.cfg16
-rw-r--r--config-model/src/test/derived/types/summary.0.cfg.part.types11255073219
-rw-r--r--config-model/src/test/derived/types/summary.cfg35
-rw-r--r--config-model/src/test/derived/types/summarymap.cfg25
-rw-r--r--config-model/src/test/derived/types/types.sd150
-rw-r--r--config-model/src/test/derived/types/vsmsummary.cfg23
-rw-r--r--config-model/src/test/derived/uri_array/ilscripts.cfg4
-rw-r--r--config-model/src/test/derived/uri_array/indexschema.cfg64
-rw-r--r--config-model/src/test/derived/uri_array/uri_array.sd8
-rw-r--r--config-model/src/test/derived/uri_wset/ilscripts.cfg4
-rw-r--r--config-model/src/test/derived/uri_wset/indexschema.cfg64
-rw-r--r--config-model/src/test/derived/uri_wset/uri_wset.sd8
-rw-r--r--config-model/src/test/examples/arrays.sd10
-rw-r--r--config-model/src/test/examples/arraysweightedsets.sd15
-rw-r--r--config-model/src/test/examples/attributeindex.sd24
-rwxr-xr-xconfig-model/src/test/examples/attributeposition.sd14
-rw-r--r--config-model/src/test/examples/attributeproperties1.sd21
-rw-r--r--config-model/src/test/examples/attributeproperties2.sd27
-rw-r--r--config-model/src/test/examples/attributesettings.sd71
-rw-r--r--config-model/src/test/examples/attributesexactmatch.sd54
-rwxr-xr-xconfig-model/src/test/examples/badstruct.sd11
-rw-r--r--config-model/src/test/examples/casing.sd64
-rw-r--r--config-model/src/test/examples/child.sd12
-rw-r--r--config-model/src/test/examples/comment.sd10
-rw-r--r--config-model/src/test/examples/desktop.sd108
-rw-r--r--config-model/src/test/examples/documentidinsummary.sd9
-rw-r--r--config-model/src/test/examples/documents.sd20
-rw-r--r--config-model/src/test/examples/duplicatenamesindoc.sd12
-rw-r--r--config-model/src/test/examples/duplicatenamesinsearchdifferenttype.sd12
-rw-r--r--config-model/src/test/examples/fieldoftypedocument.cfg74
-rw-r--r--config-model/src/test/examples/fieldoftypedocument.sd6
-rw-r--r--config-model/src/test/examples/illegalidentifiers/alias.sd38
-rw-r--r--config-model/src/test/examples/illegalidentifiers/doctypename.sd49
-rw-r--r--config-model/src/test/examples/illegalidentifiers/fieldname.sd37
-rw-r--r--config-model/src/test/examples/illegalidentifiers/rankprofile.sd41
-rw-r--r--config-model/src/test/examples/illegalidentifiers/searchname.sd37
-rw-r--r--config-model/src/test/examples/illegalidentifiers/summaryclass.sd38
-rw-r--r--config-model/src/test/examples/implicitsummaries_attribute.sd13
-rw-r--r--config-model/src/test/examples/implicitsummaryfields.sd9
-rw-r--r--config-model/src/test/examples/importing.sd34
-rw-r--r--config-model/src/test/examples/incorrectrankingexpressionfileref.sd13
-rw-r--r--config-model/src/test/examples/incorrectsummarytypes.sd19
-rw-r--r--config-model/src/test/examples/indexing.sd18
-rw-r--r--config-model/src/test/examples/indexing_attribute_changed.sd8
-rw-r--r--config-model/src/test/examples/indexing_attribute_other.sd8
-rw-r--r--config-model/src/test/examples/indexing_extra.sd13
-rw-r--r--config-model/src/test/examples/indexing_extra_field_input_extra_field.sd12
-rw-r--r--config-model/src/test/examples/indexing_extra_field_input_implicit.sd9
-rw-r--r--config-model/src/test/examples/indexing_extra_field_input_null.sd9
-rw-r--r--config-model/src/test/examples/indexing_extra_field_input_self.sd9
-rw-r--r--config-model/src/test/examples/indexing_index_changed.sd8
-rw-r--r--config-model/src/test/examples/indexing_index_other.sd8
-rw-r--r--config-model/src/test/examples/indexing_input_other_field.sd11
-rw-r--r--config-model/src/test/examples/indexing_invalid_expression.sd9
-rw-r--r--config-model/src/test/examples/indexing_modify_field_no_output.sd8
-rw-r--r--config-model/src/test/examples/indexing_multiline_output_conflict.sd21
-rw-r--r--config-model/src/test/examples/indexing_output_conflict.sd11
-rw-r--r--config-model/src/test/examples/indexing_output_other_field.sd11
-rw-r--r--config-model/src/test/examples/indexing_summary_changed.sd8
-rw-r--r--config-model/src/test/examples/indexing_summary_other.sd8
-rw-r--r--config-model/src/test/examples/indexrewrite.sd19
-rw-r--r--config-model/src/test/examples/indexsettings.sd25
-rw-r--r--config-model/src/test/examples/integerindex2attribute.sd32
-rw-r--r--config-model/src/test/examples/invalid_sd_construct.sd8
-rw-r--r--config-model/src/test/examples/invalid_sd_junk_at_end.sd10
-rw-r--r--config-model/src/test/examples/invalid_sd_lexical_error.sd12
-rw-r--r--config-model/src/test/examples/invalid_sd_no_closing_bracket.sd7
-rw-r--r--config-model/src/test/examples/invalidimplicitsummarysource.sd11
-rw-r--r--config-model/src/test/examples/invalidngram1.sd17
-rw-r--r--config-model/src/test/examples/invalidngram2.sd16
-rw-r--r--config-model/src/test/examples/invalidngram3.sd15
-rw-r--r--config-model/src/test/examples/invalidselfreferringsummary.sd9
-rw-r--r--config-model/src/test/examples/invalidsummarysource.sd11
-rw-r--r--config-model/src/test/examples/matchphase/non_existing_attribute.sd14
-rw-r--r--config-model/src/test/examples/matchphase/non_fast_search_attribute.sd14
-rw-r--r--config-model/src/test/examples/matchphase/wrong_collection_type_attribute.sd14
-rw-r--r--config-model/src/test/examples/matchphase/wrong_data_type_attribute.sd14
-rw-r--r--config-model/src/test/examples/multiplesummaries.sd33
-rw-r--r--config-model/src/test/examples/music.sd6
-rw-r--r--config-model/src/test/examples/name-check.sd20
-rw-r--r--config-model/src/test/examples/nextgen/boldedsummaryfields.sd19
-rw-r--r--config-model/src/test/examples/nextgen/dynamicsummaryfields.sd14
-rw-r--r--config-model/src/test/examples/nextgen/extrafield.sd12
-rw-r--r--config-model/src/test/examples/nextgen/implicitstructtypes.sd18
-rw-r--r--config-model/src/test/examples/nextgen/simple.sd17
-rw-r--r--config-model/src/test/examples/nextgen/summaryfield.sd15
-rw-r--r--config-model/src/test/examples/nextgen/toggleon.sd9
-rw-r--r--config-model/src/test/examples/nextgen/untransformedsummaryfields.sd15
-rw-r--r--config-model/src/test/examples/nextgen/unusedfields.sd15
-rw-r--r--config-model/src/test/examples/nextgen/uri_array.sd8
-rw-r--r--config-model/src/test/examples/nextgen/uri_simple.sd8
-rw-r--r--config-model/src/test/examples/nextgen/uri_wset.sd8
-rw-r--r--config-model/src/test/examples/ngram.sd32
-rw-r--r--config-model/src/test/examples/outsidedoc.sd18
-rw-r--r--config-model/src/test/examples/outsidesummary.sd45
-rw-r--r--config-model/src/test/examples/position_array.sd8
-rw-r--r--config-model/src/test/examples/position_attribute.sd8
-rw-r--r--config-model/src/test/examples/position_extra.sd11
-rw-r--r--config-model/src/test/examples/position_index.sd8
-rw-r--r--config-model/src/test/examples/position_summary.sd8
-rw-r--r--config-model/src/test/examples/rankingexpressionfunction/rankingexpressionfunction.sd39
-rw-r--r--config-model/src/test/examples/rankingexpressionfunction/titlematch.expression1
-rw-r--r--config-model/src/test/examples/rankingexpressioninfile/rankingexpressioninfile.sd20
-rw-r--r--config-model/src/test/examples/rankmodifier/literal.sd33
-rw-r--r--config-model/src/test/examples/rankpropvars.sd80
-rw-r--r--config-model/src/test/examples/reserved_words_as_field_names.sd16
-rw-r--r--config-model/src/test/examples/simple-with-weird-name.sd13
-rw-r--r--config-model/src/test/examples/simple.sd147
-rw-r--r--config-model/src/test/examples/stemmingdefault.sd8
-rw-r--r--config-model/src/test/examples/stemmingresolver.sd16
-rw-r--r--config-model/src/test/examples/stemmingsetting.sd36
-rw-r--r--config-model/src/test/examples/strange.sd24
-rwxr-xr-xconfig-model/src/test/examples/struct.sd17
-rw-r--r--config-model/src/test/examples/struct_outside.sd11
-rwxr-xr-xconfig-model/src/test/examples/structanddocumentwithsamenames.sd14
-rw-r--r--config-model/src/test/examples/structoutsideofdocument.sd16
-rwxr-xr-xconfig-model/src/test/examples/structresult.cfg69
-rw-r--r--config-model/src/test/examples/summaryfieldcollision.sd26
-rw-r--r--config-model/src/test/examples/wrongending.expr1
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java379
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/ConfigModelBuilderTest.java121
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/ConfigModelContextTest.java44
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/ConfigModelUtilsTest.java59
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/MapConfigModelRegistryTest.java88
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/MockModelContext.java103
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/QrserverAndGatewayPortAllocationTest.java36
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/application/provider/SchemaValidatorTest.java69
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/builder/xml/ConfigModelIdTest.java67
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/builder/xml/test/DomBuilderTest.java29
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/deploy/DeployStateTest.java148
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/deploy/SystemModelTestCase.java183
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/graph/GraphMock.java87
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/graph/ModelGraphTest.java118
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/producer/AbstractConfigProducerTest.java74
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/provision/HostSpecTest.java45
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/provision/HostsXmlProvisionerTest.java124
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java1108
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/provision/SingleNodeProvisionerTest.java99
-rw-r--r--config-model/src/test/java/com/yahoo/config/model/test/MockHosts.java19
-rw-r--r--config-model/src/test/java/com/yahoo/document/test/SDDocumentTypeTestCase.java129
-rw-r--r--config-model/src/test/java/com/yahoo/document/test/SDFieldTestCase.java60
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/ArraysTestCase.java34
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/ArraysWeightedSetsTestCase.java41
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/AttributeSettingsTestCase.java91
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/CommentTestCase.java25
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/DiversityTestCase.java111
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/FieldOfTypeDocumentTestCase.java61
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/IncorrectRankingExpressionFileRefTestCase.java32
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/IncorrectSummaryTypesTestCase.java27
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/IndexSettingsTestCase.java37
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/IndexingParsingTestCase.java32
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/MultipleSummariesTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/NameFieldCheckTestCase.java52
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/OutsideTestCase.java30
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/PredicateDataTypeTestCase.java196
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/RankProfileRegistryTest.java58
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/RankProfileTestCase.java165
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/RankPropertiesTestCase.java78
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionConstantsTestCase.java218
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionInliningTestCase.java118
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionValidationTestCase.java54
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/ReservedWordsAsFieldNamesTestCase.java23
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/searchdefinition/SDDocumentTypeOrdererTestCase.java76
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionTestCase.java68
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionsParsingTestCase.java108
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/SearchImporterTestCase.java180
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/StemmingSettingTestCase.java55
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/searchdefinition/StructTestCase.java53
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/UrlFieldValidationTestCase.java34
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/AbstractExportingTestCase.java183
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/searchdefinition/derived/AnnotationsTestCase.java69
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/ArraysTestCase.java23
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributeListTestCase.java71
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributesTestCase.java21
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/CasingTestCase.java36
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/CombinedAttributeAndIndexSearchTestCase.java21
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/DeriverTestCase.java38
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/DocumentDeriverTestCase.java114
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/EmptyRankProfileTestCase.java38
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/ExactMatchTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/ExportingTestCase.java142
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/GeminiTestCase.java62
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/IdTestCase.java45
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/IndexSchemaTestCase.java209
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/InheritanceTestCase.java179
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/IntegerAttributeToStringIndexTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/LiteralBoostTestCase.java108
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/MailTestCase.java54
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/MultipleSummariesTestCase.java21
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionsTestCase.java91
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/searchdefinition/derived/OrderIlscriptsTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/PrefixExactAttributeTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/RankProfilesTestCase.java19
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/RankPropertiesTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/SearchOrdererTestCase.java109
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/SimpleInheritTestCase.java49
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/SortingTestCase.java19
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/searchdefinition/derived/StreamingStructTestCase.java39
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/searchdefinition/derived/StructAnyOrderTestCase.java17
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryMapTestCase.java152
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryTestCase.java87
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/TwoStreamingStructsTestCase.java33
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/TypeConversionTestCase.java42
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/derived/TypesTestCase.java21
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertIndexingScript.java43
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertSearchBuilder.java29
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributeIndexTestCase.java34
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributePropertiesTestCase.java37
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributesExactMatchTestCase.java40
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/BoldingTestCase.java33
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSearchFieldsTestCase.java104
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitStructTypesTestCase.java71
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummariesTestCase.java58
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFieldsTestCase.java29
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingInputsTestCase.java44
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingOutputsTestCase.java30
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingScriptRewriterTestCase.java196
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValidationTestCase.java77
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValuesTestCase.java30
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java58
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidatorTestCase.java37
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/NGramTestCase.java84
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/PositionTestCase.java127
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/RankModifierTestCase.java22
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/RankPropertyVariablesTestCase.java42
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionWithTensorTestCase.java220
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionsTestCase.java63
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/ReservedDocumentNamesTestCase.java27
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java60
-rw-r--r--config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorFieldTestCase.java50
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/documentmodel/DocumentModelBuilderTestCase.java66
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/HostResourceTest.java105
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/InstanceResolverTest.java228
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/RecentLogFilterTest.java42
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/VespaModelFactoryTest.java257
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/admin/AdminTestCase.java256
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java440
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java76
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/ComponentValidatorTest.java57
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidatorTest.java67
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexesTest.java44
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidatorTestCase.java40
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationOverrideTest.java95
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java66
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java87
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigChangeTestUtils.java30
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidatorTest.java297
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidatorTest.java73
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java63
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidatorTest.java175
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java70
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidatorTest.java79
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java108
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/ContentClusterFixture.java72
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidatorTest.java57
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidatorTest.java170
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidatorTest.java166
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/UserConfigBuilderTest.java128
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/Bug6068056Test.java51
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2BuilderTest.java214
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilderTest.java45
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilderTest.java326
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilderTest.java817
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilderTest.java228
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilderTest.java67
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilderTest.java135
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilderTest.java47
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilderTest.java77
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilderTest.java111
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilderTest.java204
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilderTest.java33
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/searchchains/.gitignore0
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/clients/test/Gateway20TestCase.java78
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/clients/test/SpoolerTestCase.java139
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java192
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/ContainerIncludeTest.java131
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/configserver/ConfigserverClusterTest.java82
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java128
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/docproc/StandaloneDocprocContainerTest.java83
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterBindingsTest.java115
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterChainsTest.java64
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterConfigTest.java124
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/processing/test/ProcessingChainsTest.java72
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/SemanticRulesTest.java53
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/Federation2Test.java69
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/FederationTest.java113
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/MockSearchClusters.java61
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest.java135
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest2.java78
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTestBase.java25
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupTest.java89
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/common.sr21
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/other.sr5
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/PageTemplatesTestCase.java31
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfileVariantsTestCase.java105
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfilesTestCase.java122
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/empty.cfg0
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/explicit-reference-override.cfg13
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbe-query-profiles-simple.cfg19
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbesimple/scthumbnail.xml14
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfe-query-profiles-simple.cfg26
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/backend_news.xml9
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/default.xml4
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/footer.xml5
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/header.xml7
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/pages.cfg5
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richSerp.xml17
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richerSerp.xml45
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/slottingSerp.xml5
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants-configuration.cfg41
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants2-configuration.cfg61
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profiles.cfg105
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants1.xml16
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants2.xml11
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/wparent2.xml4
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/default.xml8
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/multi.xml20
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querybest.xml6
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querylove.xml5
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound-with-reference.cfg26
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound.cfg17
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java90
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilderTest.java33
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerDocumentApiBuilderTest.java173
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java573
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java83
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/DocprocBuilderTest.java226
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java234
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/container/xml/SearchBuilderTest.java194
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/ClusterTest.java762
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/ContentBaseTest.java13
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java55
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchTest.java30
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/DistributorTest.java374
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java72
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/GenericConfigTest.java79
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java298
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/IndexedSearchNodeNamingTest.java96
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/IndexedTest.java292
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/IndexingAndDocprocRoutingTest.java507
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/SearchCoverageTest.java131
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java360
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/StorageContentTest.java177
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/StorageGroupTest.java160
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/StorageNodeTest.java73
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/TuningDispatchTest.java44
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/VDSProviderTest.java38
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/YamasConfigSnoopTest.java72
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/cluster/ClusterTest.java100
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilderTest.java58
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilderTest.java77
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilderTest.java99
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/utils/ApplicationPackageBuilder.java43
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java121
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterUtils.java75
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/content/utils/SearchDefinitionBuilder.java35
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/filedistribution/FileDistributorTestCase.java34
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesModelTest.java62
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesTest.java80
-rwxr-xr-xconfig-model/src/test/java/com/yahoo/vespa/model/routing/test/RoutingTestCase.java203
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/search/MultilevelDispatchTest.java382
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/search/TldTest.java152
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java222
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentSelectionConverterTest.java32
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchClusterTest.java187
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchNodeTest.java75
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/search/utils/DispatchUtils.java35
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/storage/DistributionBitCalculatorTest.java37
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/storage/test/StorageModelTestCase.java56
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/ApiConfigModel.java69
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/ApiService.java39
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/DomTestServiceBuilder.java59
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/ModelAmendingTestCase.java168
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/ModelConfigProviderTest.java41
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/ParentService.java61
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/PortsMetaTestCase.java37
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/SimpleConfigModel.java79
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/SimpleService.java54
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/TestApi.java12
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTestCase.java341
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/utils/ApplicationPackageUtils.java94
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/utils/CommonVespaModelSetup.java34
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/utils/DeployLoggerStub.java63
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithFilePkg.java67
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithMockPkg.java75
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/utils/DurationTest.java40
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/utils/FileSenderTest.java176
-rw-r--r--config-model/src/test/java/com/yahoo/vespa/model/utils/internal/ReflectionUtilTest.java91
-rw-r--r--config-model/src/test/java/helpers/CompareConfigTestHelper.java39
-rw-r--r--config-model/src/test/processing/boldnonstring.sd14
-rw-r--r--config-model/src/test/resources/configdefinitions/anotherrestart.def5
-rw-r--r--config-model/src/test/resources/configdefinitions/arraytypes.def11
-rw-r--r--config-model/src/test/resources/configdefinitions/function-test.def73
-rw-r--r--config-model/src/test/resources/configdefinitions/nonrestart.def5
-rw-r--r--config-model/src/test/resources/configdefinitions/restart.def5
-rw-r--r--config-model/src/test/resources/configdefinitions/simpletypes.def11
-rw-r--r--config-model/src/test/resources/configdefinitions/standard.def10
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/MultipleRestApisTest.scala137
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/RestApiTest.scala214
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala60
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/search/searchchain/FederationSearcherTest.scala186
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilderTest.scala62
-rw-r--r--config-model/src/test/scala/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilderTest.scala144
-rwxr-xr-xconfig-model/src/test/schema-test-files/hosts.xml23
-rw-r--r--config-model/src/test/schema-test-files/major-version-services.xml4
-rw-r--r--config-model/src/test/schema-test-files/services-hosted-explicit-admin.xml20
-rw-r--r--config-model/src/test/schema-test-files/services-hosted.xml18
-rw-r--r--config-model/src/test/schema-test-files/services.xml175
-rw-r--r--config-model/src/test/schema-test-files/standalone-container.xml141
-rwxr-xr-xconfig-model/src/test/sh/test-schema.sh28
1577 files changed, 105989 insertions, 0 deletions
diff --git a/config-model/src/main/assembly/.gitignore b/config-model/src/main/assembly/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/main/assembly/.gitignore
diff --git a/config-model/src/main/java/com/yahoo/config/model/ApplicationConfigProducerRoot.java b/config-model/src/main/java/com/yahoo/config/model/ApplicationConfigProducerRoot.java
new file mode 100644
index 00000000000..1ef0ed6ec0a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ApplicationConfigProducerRoot.java
@@ -0,0 +1,279 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.cloud.config.*;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.cloud.config.ModelConfig.Hosts;
+import com.yahoo.cloud.config.ModelConfig.Hosts.Services;
+import com.yahoo.cloud.config.ModelConfig.Hosts.Services.Ports;
+import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.document.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.documentapi.messagebus.protocol.DocumentrouteselectorpolicyConfig;
+import com.yahoo.messagebus.MessagebusConfig;
+import com.yahoo.vespa.configmodel.producers.DocumentManager;
+import com.yahoo.vespa.configmodel.producers.DocumentTypes;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import com.yahoo.vespa.model.*;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.clients.Clients;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.filedistribution.FileDistributionConfigProducer;
+import com.yahoo.vespa.model.routing.Routing;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * This is the parent of all ConfigProducers in the system resulting from configuring an application.
+ *
+ * @author gjoranv
+ */
+public class ApplicationConfigProducerRoot extends AbstractConfigProducer<AbstractConfigProducer<?>> implements CommonConfigsProducer {
+
+ private final DocumentModel documentModel;
+ private Routing routing = null;
+ // The ConfigProducers contained in this Vespa. (configId->producer)
+ Map<String, ConfigProducer> id2producer = new LinkedHashMap<>();
+ private Admin admin = null;
+ private HostSystem hostSystem = null;
+ private final Version vespaVersion;
+ private final ApplicationId applicationId;
+
+ /**
+ * Creates and initializes a new Vespa from the service config file
+ * in the given application directory.
+ *
+ * @param parent The parent, usually VespaModel
+ * @param name The name, used as configId
+ * @param documentModel DocumentModel to serve global document config from.
+ */
+ public ApplicationConfigProducerRoot(AbstractConfigProducer parent, String name, DocumentModel documentModel, Version vespaVersion, ApplicationId applicationId) {
+ super(parent, name);
+ this.documentModel = documentModel;
+ this.vespaVersion = vespaVersion;
+ this.applicationId = applicationId;
+ }
+
+ /**
+ * @return an unmodifiable copy of the set of configIds in this VespaModel.
+ */
+ public Set<String> getConfigIds() {
+ return Collections.unmodifiableSet(id2producer.keySet());
+ }
+
+ /**
+ * Returns the ConfigProducer with the given id, or null if no such
+ * configId exists.
+ *
+ * @param configId The configId, e.g. "search.0/tld.0"
+ * @return ConfigProducer with the given configId
+ */
+ public ConfigProducer getConfigProducer(String configId) {
+ return id2producer.get(configId);
+ }
+
+ /**
+ * Returns the Service with the given id, or null if no such
+ * configId exists or if it belongs to a non-Service ConfigProducer.
+ *
+ * @param configId The configId, e.g. "search.0/tld.0"
+ * @return Service with the given configId
+ */
+ public Service getService(String configId) {
+ ConfigProducer cp = getConfigProducer(configId);
+ if (cp == null || !(cp instanceof Service)) {
+ return null;
+ }
+ return (Service) cp;
+ }
+
+ /**
+ * Adds the descendant (at any depth level), so it can be looked up
+ * on configId in the Map.
+ *
+ * @param descendant The configProducer descendant to add
+ */
+ // TODO: Make protected if this moves to the same package as AbstractConfigProducer
+ public void addDescendant(AbstractConfigProducer descendant) {
+ id2producer.put(descendant.getConfigId(), descendant);
+ }
+
+ /**
+ * Prepares the model for start. The {@link VespaModel} calls
+ * this methods after it has loaded this and all plugins have been loaded and
+ * their initialize() methods have been called.
+ *
+ * @param plugins All initialized plugins of the vespa model.
+ */
+ public void prepare(ConfigModelRepo plugins) {
+ if (routing != null) {
+ routing.deriveCommonSettings(plugins);
+ }
+ }
+
+ public void setupAdmin(Admin admin) {
+ this.admin = admin;
+ }
+
+ public void setupRouting(ConfigModelRepo configModels) {
+ if (admin != null) {
+ Routing routing = configModels.getRouting();
+ if (routing == null) {
+ routing = new Routing(ConfigModelContext.createFromParentAndId(configModels, this, "routing"));
+ configModels.add(routing);
+ }
+ this.routing = routing;
+ }
+ }
+
+ @Override
+ public void getConfig(DocumentmanagerConfig.Builder builder) {
+ new DocumentManager().produce(documentModel, builder);
+ }
+
+ @Override
+ public void getConfig(DocumenttypesConfig.Builder builder) {
+ new DocumentTypes().produce(documentModel, builder);
+ }
+
+ @Override
+ public void getConfig(DocumentrouteselectorpolicyConfig.Builder builder) {
+ if (routing != null) {
+ routing.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(MessagebusConfig.Builder builder) {
+ if (routing != null) {
+ routing.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(LogdConfig.Builder builder) {
+ if (admin != null) {
+ admin.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(SlobroksConfig.Builder builder) {
+ if (admin != null) {
+ admin.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(ZookeepersConfig.Builder builder) {
+ if (admin != null) {
+ admin.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(LoadTypeConfig.Builder builder) {
+ VespaModel model = (VespaModel) getRoot();
+ Clients clients = model.getClients();
+ if (clients != null) {
+ clients.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(ClusterListConfig.Builder builder) {
+ VespaModel model = (VespaModel) getRoot();
+ for (ContentCluster cluster : model.getContentClusters().values()) {
+ ClusterListConfig.Storage.Builder storage = new ClusterListConfig.Storage.Builder();
+ storage.name(cluster.getName());
+ storage.configid(cluster.getConfigId());
+ builder.storage(storage);
+ }
+ }
+
+ @Override
+ public void getConfig(ModelConfig.Builder builder) {
+ builder.vespaVersion(vespaVersion.toSerializedForm());
+ for (HostResource modelHost : getHostSystem().getHosts()) {
+ builder.hosts(new Hosts.Builder()
+ .name(modelHost.getHostName())
+ .services(getServices(modelHost))
+ );
+ }
+ }
+
+ private List<Services.Builder> getServices(HostResource modelHost) {
+ List<Services.Builder> ret = new ArrayList<>();
+ for (Service modelService : modelHost.getServices()) {
+ ret.add(new Services.Builder()
+ .name(modelService.getServiceName())
+ .type(modelService.getServiceType())
+ .configid(modelService.getConfigId())
+ .clustertype(modelService.getServicePropertyString("clustertype", ""))
+ .clustername(modelService.getServicePropertyString("clustername", ""))
+ .index(Integer.parseInt(modelService.getServicePropertyString("index", "999999")))
+ .ports(getPorts(modelService))
+ );
+ }
+ return ret;
+ }
+
+ private List<Ports.Builder> getPorts(Service modelService) {
+ List<Ports.Builder> ret = new ArrayList<>();
+ PortsMeta portsMeta = modelService.getPortsMeta();
+ for (int i = 0; i < portsMeta.getNumPorts(); i++) {
+ ret.add(new Ports.Builder()
+ .number(modelService.getRelativePort(i))
+ .tags(getPortTags(portsMeta, i))
+ );
+ }
+ return ret;
+ }
+
+ public static String getPortTags(PortsMeta portsMeta, int portNumber) {
+ StringBuilder sb = new StringBuilder();
+ boolean firstTag = true;
+ for (String s : portsMeta.getTagsAt(portNumber)) {
+ if (!firstTag) {
+ sb.append(" ");
+ } else {
+ firstTag = false;
+ }
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ public void setHostSystem(HostSystem hostSystem) {
+ this.hostSystem = hostSystem;
+ }
+
+ @Override
+ public HostSystem getHostSystem() {
+ return hostSystem;
+ }
+
+ public FileDistributionConfigProducer getFileDistributionConfigProducer() {
+ return admin.getFileDistributionConfigProducer();
+ }
+
+ public Admin getAdmin() {
+ return admin;
+ }
+
+ @Override
+ public void getConfig(ApplicationIdConfig.Builder builder) {
+ builder.tenant(applicationId.tenant().value());
+ builder.application(applicationId.application().value());
+ builder.instance(applicationId.instance().value());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/CommonConfigsProducer.java b/config-model/src/main/java/com/yahoo/config/model/CommonConfigsProducer.java
new file mode 100644
index 00000000000..228c74480c4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/CommonConfigsProducer.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.cloud.config.ApplicationIdConfig;
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.cloud.config.SlobroksConfig;
+import com.yahoo.cloud.config.ClusterListConfig;
+import com.yahoo.cloud.config.ZookeepersConfig;
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.document.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.documentapi.messagebus.protocol.DocumentrouteselectorpolicyConfig;
+import com.yahoo.messagebus.MessagebusConfig;
+
+
+/**
+ * This interface describes the configs that are produced by the model producer root.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface CommonConfigsProducer extends DocumentmanagerConfig.Producer,
+ DocumenttypesConfig.Producer,
+ MessagebusConfig.Producer,
+ DocumentrouteselectorpolicyConfig.Producer,
+ LogdConfig.Producer,
+ SlobroksConfig.Producer,
+ ZookeepersConfig.Producer,
+ LoadTypeConfig.Producer,
+ ClusterListConfig.Producer,
+ ModelConfig.Producer,
+ ApplicationIdConfig.Producer {
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/ConfigModel.java b/config-model/src/main/java/com/yahoo/config/model/ConfigModel.java
new file mode 100644
index 00000000000..10342d44914
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ConfigModel.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+/**
+ * A config model is an abstract representation of a subsystem, which is used
+ * by the builder of that subsystem to derive a config producer tree for the subsystem, and by other
+ * builders to access information about the subsystem for config production at a suitable abstraction level.
+ *
+ * @author gjoranv
+ * @author bratseth
+ * @author lulf
+ */
+public abstract class ConfigModel {
+
+ private final String id;
+
+ /**
+ * Constructs a new config model given a context.
+ *
+ * @param modelContext The model context.
+ */
+ public ConfigModel(ConfigModelContext modelContext) {
+ super();
+ this.id = modelContext.getProducerId();
+ }
+
+ /** Returns the id of this model */
+ public final String getId() { return id; }
+
+ /**
+ * Initializes this model. All inter-model independent initialization
+ * is done by implementing this method.
+ * The model will be made available to dependent models by the framework when this returns.
+ * <p>
+ * TODO: Remove this method, as this is now done by the model builders.
+ *
+ * This default implementation does nothing.
+ *
+ * @param configModelRepo The ConfigModelRepo of the VespaModel
+ * @deprecated This will go away in the next Vespa major release. Instead, inject the models you depend on
+ * in your config model constructor.
+ */
+ public void initialize(ConfigModelRepo configModelRepo) { return; }
+
+ /**
+ * Prepares this model to start serving config requests, possibly using properties of other models.
+ * The framework will call this method after models have been built. The model
+ * should finalize its configurations that depend on other models in this step.
+ *
+ * This default implementation does nothing.
+ *
+ * @param configModelRepo The ConfigModelRepo of the system model
+ */
+ public void prepare(ConfigModelRepo configModelRepo) { return; }
+
+ /**
+ * <p>Returns whether this model must be maintained in memory for serving config requests.
+ * Models which are used to amend other models at build time should override this to return false.</p>
+ *
+ * <p>This default implementation returns true.</p>
+ */
+ public boolean isServing() { return true; }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/ConfigModelContext.java b/config-model/src/main/java/com/yahoo/config/model/ConfigModelContext.java
new file mode 100644
index 00000000000..c375d8e28fa
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ConfigModelContext.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * This class contains a context that is passed to a model builder, and can be used to retrieve the application package,
+ * logger etc.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigModelContext {
+
+ private final AbstractConfigProducer producer;
+ private final String producerId;
+ private final DeployState deployState;
+ private final ConfigModelRepoAdder configModelRepoAdder;
+
+ private ConfigModelContext(DeployState deployState, ConfigModelRepoAdder configModelRepoAdder,
+ AbstractConfigProducer producer, String producerId) {
+ this.deployState = deployState;
+ this.configModelRepoAdder = configModelRepoAdder;
+ this.producer = producer;
+ this.producerId = producerId;
+ }
+
+ public ApplicationPackage getApplicationPackage() { return deployState.getApplicationPackage(); }
+ public String getProducerId() { return producerId; }
+ public AbstractConfigProducer getParentProducer() { return producer; }
+ public DeployLogger getDeployLogger() { return deployState.getDeployLogger(); }
+ public DeployState getDeployState() { return deployState; }
+
+ /** Returns write access to the config model repo, or null (only) if this is improperly initialized during testing */
+ public ConfigModelRepoAdder getConfigModelRepoAdder() { return configModelRepoAdder; }
+
+ /**
+ * Create a new context with a different parent, but with the same id and application package.
+ *
+ * @param newParent The parent to use for the new context.
+ * @return A new context.
+ */
+ public ConfigModelContext modifyParent(AbstractConfigProducer newParent) {
+ return ConfigModelContext.create(deployState, configModelRepoAdder, newParent, producerId);
+ }
+
+ /**
+ * Create an application context from a parent producer and an id.
+ * @param deployState The global deploy state for this model.
+ * @param parent The parent to be used for the config model.
+ * @param id The id to be used for the config model.
+ * @return An model context that can be passed to a model.
+ */
+ public static ConfigModelContext create(DeployState deployState, ConfigModelRepoAdder configModelRepoAdder,
+ AbstractConfigProducer parent, String id) {
+ return new ConfigModelContext(deployState, configModelRepoAdder, parent, id);
+ }
+
+ /**
+ * Create an application context from a parent producer and an id.
+ * @param parent The parent to be used for the config model.
+ * @param id The id to be used for the config model.
+ * @return An model context that can be passed to a model.
+ */
+ public static ConfigModelContext createFromParentAndId(ConfigModelRepoAdder configModelRepoAdder, AbstractConfigProducer parent, String id) {
+ return create(parent.getRoot().getDeployState(), configModelRepoAdder, parent, id);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/ConfigModelInstanceFactory.java b/config-model/src/main/java/com/yahoo/config/model/ConfigModelInstanceFactory.java
new file mode 100644
index 00000000000..a9b3fc73eb3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ConfigModelInstanceFactory.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+/**
+ * Interface for factories of config models.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public interface ConfigModelInstanceFactory<MODEL extends ConfigModel> {
+
+ /**
+ * Create an instance of {@link com.yahoo.config.model.ConfigModel} given the input context.
+ *
+ * @param context The {@link com.yahoo.config.model.ConfigModelContext} to use.
+ * @return an instance of {@link com.yahoo.config.model.ConfigModel}
+ */
+ MODEL createModel(ConfigModelContext context);
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/ConfigModelRegistry.java b/config-model/src/main/java/com/yahoo/config/model/ConfigModelRegistry.java
new file mode 100644
index 00000000000..2735b9f7fdf
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ConfigModelRegistry.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * A resolver of implementations of named config models.
+ * Registries may be chained in a chain of command.
+ *
+ * @author bratseth
+ */
+public abstract class ConfigModelRegistry {
+
+ private final ConfigModelRegistry chained;
+
+ public ConfigModelRegistry() {
+ this(new EmptyTerminalRegistry());
+ }
+
+ /** Creates a config model class registry which forwards unresolved requests to the argument instance */
+ public ConfigModelRegistry(ConfigModelRegistry chained) {
+ this.chained=chained;
+ }
+
+ /**
+ * Returns the builders this id resolves to both in this and any chained registry.
+ *
+ * @return the resolved config model builders, or an empty list (never null) if none
+ */
+ public abstract Collection<ConfigModelBuilder> resolve(ConfigModelId id);
+
+ public ConfigModelRegistry chained() { return chained; }
+
+ /** An empty registry which does not support chaining */
+ private static class EmptyTerminalRegistry extends ConfigModelRegistry {
+
+ public EmptyTerminalRegistry() {
+ super(null);
+ }
+
+ @Override
+ public Collection<ConfigModelBuilder> resolve(ConfigModelId id) {
+ return Collections.emptyList();
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/ConfigModelRepo.java b/config-model/src/main/java/com/yahoo/config/model/ConfigModelRepo.java
new file mode 100644
index 00000000000..fa46c33a97f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ConfigModelRepo.java
@@ -0,0 +1,256 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.config.model.graph.ModelGraphBuilder;
+import com.yahoo.config.model.graph.ModelNode;
+import com.yahoo.config.model.provision.HostsXmlProvisioner;
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.VespaModelBuilder;
+import com.yahoo.vespa.model.clients.Clients;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.routing.Routing;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * A collection of config model instances owned by a system model
+ *
+ * @author gjoranv
+ */
+public class ConfigModelRepo implements ConfigModelRepoAdder, Serializable, Iterable<ConfigModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final Logger log = Logger.getLogger(ConfigModelRepo.class.getPackage().toString());
+
+ private final Map<String,ConfigModel> configModelMap = new TreeMap<>();
+ private final List<ConfigModel> configModels = new ArrayList<>();
+
+ /**
+ * Returns a config model for a given id
+ *
+ * @param id the id of the model to return
+ * @return the model, or none if a model with this id is not present in this
+ */
+ public ConfigModel get(String id) {
+ return configModelMap.get(id);
+ }
+
+ /** Adds a new config model instance in this */
+ @Override
+ public void add(ConfigModel model) {
+ configModelMap.put(model.getId(), model);
+ configModels.add(model);
+ }
+
+ /** Returns the models in this as an iterator */
+ public Iterator<ConfigModel> iterator() {
+ return configModels.iterator();
+ }
+
+ /** Returns a read-only view of the config model instances of this */
+ public Map<String,ConfigModel> asMap() { return Collections.unmodifiableMap(configModelMap); }
+
+ /** Initialize part 1.: Reads the config models used in the application package. */
+ public void readConfigModels(DeployState deployState, VespaModelBuilder builder,
+ ApplicationConfigProducerRoot root, ConfigModelRegistry configModelRegistry) throws IOException, SAXException {
+ Element userServicesElement = getServicesFromApp(deployState.getApplicationPackage());
+ readConfigModels(root, userServicesElement, deployState, configModelRegistry);
+ builder.postProc(root, this);
+ }
+
+ private Element getServicesFromApp(ApplicationPackage applicationPackage) throws IOException, SAXException {
+ try (Reader servicesFile = applicationPackage.getServices()) {
+ return getServicesFromReader(servicesFile);
+ }
+ }
+
+ /**
+ * If the top level is &lt;services&gt;, it contains a list of services elements,
+ * otherwise, the top level tag is a single service.
+ */
+ private List<Element> getServiceElements(Element servicesRoot) {
+ if (servicesRoot.getTagName().equals("services"))
+ return XML.getChildren(servicesRoot);
+
+ List<Element> singleServiceList = new ArrayList<>(1);
+ singleServiceList.add(servicesRoot);
+ return singleServiceList;
+ }
+
+ /**
+ * Creates all the config models specified in the given XML element and
+ * passes their respective XML node as parameter.
+ *
+ * @param root The Root to set as parent for all plugins
+ * @param servicesRoot XML root node of the services file
+ */
+ private void readConfigModels(ApplicationConfigProducerRoot root, Element servicesRoot, DeployState deployState, ConfigModelRegistry configModelRegistry) throws IOException, SAXException {
+ final Map<ConfigModelBuilder, List<Element>> model2Element = new LinkedHashMap<>();
+ ModelGraphBuilder graphBuilder = new ModelGraphBuilder();
+
+ final List<Element> children = getServiceElements(servicesRoot);
+
+ if (XML.getChild(servicesRoot, "admin") == null)
+ children.add(getImplicitAdmin(deployState));
+
+ children.addAll(getPermanentServices(deployState));
+
+ for (Element servicesElement : children) {
+ String tagName = servicesElement.getTagName();
+ if (tagName.equals("config")) continue; // TODO: Remove on Vespa 6
+ if (tagName.equals("cluster")) continue; // TODO: Remove on Vespa 6
+ if ((tagName.equals("clients")) && deployState.isHostedVespa())
+ throw new IllegalArgumentException("<" + tagName + "> is not allowed when running Vespa in a hosted environment");
+
+ String tagVersion = servicesElement.getAttribute("version");
+ ConfigModelId xmlId = ConfigModelId.fromNameAndVersion(tagName, tagVersion);
+
+ Collection<ConfigModelBuilder> builders = configModelRegistry.resolve(xmlId);
+
+ if (builders.isEmpty())
+ throw new RuntimeException("Could not resolve tag <" + tagName + " version=\"" + tagVersion + "\"> to a config model component");
+
+ for (ConfigModelBuilder builder : builders) {
+ if ( ! model2Element.containsKey(builder)) {
+ model2Element.put(builder, new ArrayList<>());
+ graphBuilder.addBuilder(builder);
+ }
+ model2Element.get(builder).add(servicesElement);
+ }
+ }
+
+ for (ModelNode node : graphBuilder.build().topologicalSort())
+ buildModels(node, deployState, root, model2Element.get(node.builder));
+ for (ConfigModel model : configModels)
+ model.initialize(ConfigModelRepo.this);
+ }
+
+ private Collection<Element> getPermanentServices(DeployState deployState) throws IOException, SAXException {
+ List<Element> permanentServices = new ArrayList<>();
+ Optional<ApplicationPackage> applicationPackage = deployState.getPermanentApplicationPackage();
+ if (applicationPackage.isPresent()) {
+ ApplicationFile file = applicationPackage.get().getFile(Path.fromString(ApplicationPackage.PERMANENT_SERVICES));
+ if (file.exists()) {
+ try (Reader reader = file.createReader()) {
+ Element permanentServicesRoot = getServicesFromReader(reader);
+ permanentServices.addAll(getServiceElements(permanentServicesRoot));
+ }
+ }
+ }
+ return permanentServices;
+ }
+
+ private Element getServicesFromReader(Reader reader) throws IOException, SAXException {
+ Document doc = XmlHelper.getDocumentBuilder().parse(new InputSource(reader));
+ return doc.getDocumentElement();
+ }
+
+ private void buildModels(ModelNode node, DeployState deployState, AbstractConfigProducer parent, List<Element> elements) {
+ for (Element servicesElement : elements) {
+ ConfigModel model = buildModel(node, deployState, parent, servicesElement);
+ if (model.isServing())
+ add(model);
+ }
+ }
+
+ private ConfigModel buildModel(ModelNode node, DeployState deployState, AbstractConfigProducer parent, Element servicesElement) {
+ ConfigModelBuilder builder = node.builder;
+ ConfigModelContext context = ConfigModelContext.create(deployState, this, parent, getIdString(servicesElement));
+ return builder.build(node, servicesElement, context);
+ }
+
+ private static String getIdString(Element spec) {
+ String idString = XmlHelper.getIdString(spec);
+ if (idString == null || idString.isEmpty()) {
+ idString = spec.getTagName();
+ }
+ return idString;
+ }
+
+ /**
+ * Initialize part 2.:
+ * Prepare all config models for starting. Must be called after plugins are loaded and frozen.
+ */
+ public void prepareConfigModels() {
+ for (ConfigModel model : configModels) {
+ model.prepare(ConfigModelRepo.this);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T extends ConfigModel> List<T> getModels(Class<T> modelClass) {
+ List<T> modelsOfModelClass = new ArrayList<>();
+
+ for (ConfigModel model : asMap().values()) {
+ if (modelClass.isInstance(model))
+ modelsOfModelClass.add((T)model);
+ }
+ return modelsOfModelClass;
+ }
+
+ public Clients getClients() {
+ for (ConfigModel m : configModels) {
+ if (m instanceof Clients) {
+ return (Clients)m;
+ }
+ }
+ return null;
+ }
+
+ public Routing getRouting() {
+ for (ConfigModel m : configModels) {
+ if (m instanceof Routing) {
+ return (Routing)m;
+ }
+ }
+ return null;
+ }
+
+ public Content getContent() {
+ for (ConfigModel m : configModels) {
+ if (m instanceof Content) {
+ return (Content)m;
+ }
+ }
+ return null;
+ }
+
+ // TODO: Doctoring on the XML is the wrong level for this. We should be able to mark a model as default instead -Jon
+ private static Element getImplicitAdmin(DeployState deployState) throws IOException, SAXException {
+ final boolean hostedVespa = deployState.isHostedVespa();
+ String defaultAdminElement = hostedVespa ? getImplicitAdminV4() : getImplicitAdminV2();
+ log.log(LogLevel.DEBUG, "No <admin> defined, using " + defaultAdminElement);
+ return XmlHelper.getDocumentBuilder().parse(new InputSource(new StringReader(defaultAdminElement))).getDocumentElement();
+ }
+
+ private static String getImplicitAdminV2() {
+ return "<admin version='2.0'>\n" +
+ " <adminserver hostalias='" + HostsXmlProvisioner.IMPLICIT_ADMIN_HOSTALIAS + "'/>\n" +
+ "</admin>\n";
+ }
+
+ private static String getImplicitAdminV4() {
+ return "<admin version='4.0'>\n" +
+ " <nodes count='1' />\n" +
+ "</admin>\n";
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/ConfigModelRepoAdder.java b/config-model/src/main/java/com/yahoo/config/model/ConfigModelRepoAdder.java
new file mode 100644
index 00000000000..93c591b3a8e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ConfigModelRepoAdder.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+/**
+ * An interface which provides addition of new config models.
+ * This exists because some models need to add additional models during the build phase so *write* access
+ * to the config model repo is needed. *Read* access, on the other hand needs to happen through config model dependency
+ * inkection to avoid circular dependencies or undeclared dependencies working by accident.
+ *
+ * @author bratseth
+ */
+public interface ConfigModelRepoAdder {
+
+ void add(ConfigModel model);
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/ConfigModelUtils.java b/config-model/src/main/java/com/yahoo/config/model/ConfigModelUtils.java
new file mode 100644
index 00000000000..f64eee6e863
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/ConfigModelUtils.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.Serializable;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static javax.xml.xpath.XPathConstants.BOOLEAN;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * Utilities for config models
+ *
+ * @author gjoranv
+ */
+// TODO: Split this into appropriate classes, or move to ConfigModel superclass
+public class ConfigModelUtils implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public static Pattern hourNmin = Pattern.compile("(\\d\\d):(\\d\\d)");
+
+ public static Map<String, Integer> day2int;
+ static {
+ day2int = new HashMap<>();
+ day2int.put("sunday", 0);
+ day2int.put("monday", 1);
+ day2int.put("tuesday", 2);
+ day2int.put("wednesday", 3);
+ day2int.put("thursday", 4);
+ day2int.put("friday", 5);
+ day2int.put("saturday", 6);
+ }
+
+ /** Parses a 24 hour clock that must be the five characters ##:## to an int stating minutes after midnight. */
+ public static int getTimeOfDay(String time) {
+ Matcher m = ConfigModelUtils.hourNmin.matcher(time);
+ if (m.matches()) {
+ return Integer.parseInt(m.group(1)) * 60 + Integer.parseInt(m.group(2));
+ }
+ throw new IllegalArgumentException("The string '" + time + "' is not in ##:## format.");
+ }
+
+ /** Parses a day of week name in english to an int, where 0 is sunday, 6 saturday. */
+ public static int getDayOfWeek(String day) {
+ return ConfigModelUtils.day2int.get(toLowerCase(day));
+ }
+
+ /**
+ * Create a string with link to documentation for latest release.
+ *
+ * @param filePath Relative path of the file to link to, e.g. reference/services-jdisc.html
+ * @return a String with link to documentation
+ */
+ public static String createDocLink(String filePath) {
+ return filePath;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/MapConfigModelRegistry.java b/config-model/src/main/java/com/yahoo/config/model/MapConfigModelRegistry.java
new file mode 100644
index 00000000000..d47a009928d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/MapConfigModelRegistry.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.google.inject.Inject;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.log.LogLevel;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class MapConfigModelRegistry extends ConfigModelRegistry {
+
+ private static final Logger log = Logger.getLogger(MapConfigModelRegistry.class.getPackage().getName());
+ private final List<ConfigModelBuilder> builders;
+
+ /**
+ * Constructs a registry of config models, where the components are injected.
+ *
+ * @param registry a component registry
+ */
+ @Inject
+ public MapConfigModelRegistry(ComponentRegistry<? extends ConfigModelBuilder> registry) {
+ this(registry.allComponents());
+ }
+
+ /**
+ * Constructs a registry of config models.
+ *
+ * @param builderCollection A collection of builders used to populate the registry.
+ */
+ public MapConfigModelRegistry(Collection<? extends ConfigModelBuilder> builderCollection) {
+ super();
+ builders = new ArrayList<>(builderCollection);
+ }
+
+ @Override
+ public Collection<ConfigModelBuilder> resolve(ConfigModelId id) {
+ Set<ConfigModelBuilder> matchingBuilders = new HashSet<>(chained().resolve(id));
+ for (ConfigModelBuilder builder : builders)
+ if (builder.handlesElements().contains(id))
+ matchingBuilders.add(builder);
+ return matchingBuilders;
+ }
+
+ /**
+ * Create a registry from a variable argument list of builders.
+ *
+ * @param builders A variable argument list of builders to use in this map
+ * @return a ConfigModelRegistry instance.
+ */
+ public static ConfigModelRegistry createFromList(ConfigModelBuilder<? extends ConfigModel> ... builders) {
+ return new MapConfigModelRegistry(Arrays.asList(builders));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/NullConfigModelRegistry.java b/config-model/src/main/java/com/yahoo/config/model/NullConfigModelRegistry.java
new file mode 100644
index 00000000000..6930c8c792b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/NullConfigModelRegistry.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+
+import java.util.Collection;
+
+/**
+ * A config model class registry that only forwards to the chained registry.
+ *
+ * @author bratseth
+ */
+public class NullConfigModelRegistry extends ConfigModelRegistry {
+
+ @Override
+ public Collection<ConfigModelBuilder> resolve(ConfigModelId id) {
+ return chained().resolve(id);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/admin/AdminModel.java b/config-model/src/main/java/com/yahoo/config/model/admin/AdminModel.java
new file mode 100644
index 00000000000..8b6ea27ccab
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/admin/AdminModel.java
@@ -0,0 +1,113 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.admin;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.builder.xml.dom.DomAdminV2Builder;
+import com.yahoo.vespa.model.builder.xml.dom.DomAdminV4Builder;
+import com.yahoo.vespa.model.container.ContainerModel;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * Config model adaptor of the Admin class.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class AdminModel extends ConfigModel {
+
+ private Admin admin = null;
+ private final Collection<ContainerModel> containerModels;
+
+ /**
+ * Constructs a new config model given a context.
+ *
+ * @param modelContext the model context.
+ */
+ public AdminModel(ConfigModelContext modelContext, Collection<ContainerModel> containerModels) {
+ super(modelContext);
+ this.containerModels = containerModels;
+ }
+
+ public Admin getAdmin() { return admin; }
+
+ private Collection<ContainerModel> getContainerModels() { return containerModels; }
+
+ @Override
+ public void prepare(ConfigModelRepo configModelRepo) {
+ verifyClusterControllersOnlyDefinedForContent(configModelRepo);
+ if (admin == null || admin.getClusterControllers() == null) return;
+ admin.getClusterControllers().prepare();
+ }
+
+ private void verifyClusterControllersOnlyDefinedForContent(ConfigModelRepo configModelRepo) {
+ Admin admin = getAdmin();
+ if (admin == null || admin.getClusterControllers() == null) return;
+ if (configModelRepo.getContent() == null) {
+ throw new IllegalArgumentException("Declaring <clustercontrollers> in <admin> in services.xml will not work when <content> is not defined");
+ }
+ }
+
+ public static class BuilderV2 extends ConfigModelBuilder<AdminModel> {
+
+ public static final List<ConfigModelId> configModelIds =
+ ImmutableList.of(ConfigModelId.fromNameAndVersion("admin", "2.0"),
+ ConfigModelId.fromNameAndVersion("admin", "1.0"));
+
+ public BuilderV2() {
+ super(AdminModel.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() { return configModelIds; }
+
+ @Override
+ public void doBuild(AdminModel model, Element adminElement, ConfigModelContext modelContext) {
+ AbstractConfigProducer parent = modelContext.getParentProducer();
+ DeployProperties properties = modelContext.getDeployState().getProperties();
+ DomAdminV2Builder domBuilder = new DomAdminV2Builder(modelContext.getDeployState().getFileRegistry(), properties.multitenant(), properties.configServerSpecs());
+ model.admin = domBuilder.build(parent, adminElement);
+ // TODO: Is required since other models depend on admin.
+ if (parent instanceof ApplicationConfigProducerRoot) {
+ ((ApplicationConfigProducerRoot)parent).setupAdmin(model.admin);
+ }
+ }
+ }
+
+ public static class BuilderV4 extends ConfigModelBuilder<AdminModel> {
+
+ public static final List<ConfigModelId> configModelIds =
+ ImmutableList.of(ConfigModelId.fromNameAndVersion("admin", "3.0"),
+ ConfigModelId.fromNameAndVersion("admin", "4.0"));
+
+ public BuilderV4() {
+ super(AdminModel.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() { return configModelIds; }
+
+ @Override
+ public void doBuild(AdminModel model, Element adminElement, ConfigModelContext modelContext) {
+ AbstractConfigProducer parent = modelContext.getParentProducer();
+ DeployProperties properties = modelContext.getDeployState().getProperties();
+ DomAdminV4Builder domBuilder = new DomAdminV4Builder(modelContext, properties.multitenant(), properties.configServerSpecs(), model.getContainerModels());
+ model.admin = domBuilder.build(parent, adminElement);
+ // TODO: Is required since other models depend on admin.
+ if (parent instanceof ApplicationConfigProducerRoot) {
+ ((ApplicationConfigProducerRoot)parent).setupAdmin(model.admin);
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelBuilder.java b/config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelBuilder.java
new file mode 100644
index 00000000000..a0b1be20df6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelBuilder.java
@@ -0,0 +1,119 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.builder.xml;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelInstanceFactory;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.api.ConfigModelPlugin;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import org.w3c.dom.Element;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+/**
+ * Builds a config model using DOM parsers
+ *
+ * @author vegardh
+ * @since 5.1.10
+ */
+public abstract class ConfigModelBuilder<MODEL extends ConfigModel> extends AbstractComponent implements ConfigModelPlugin {
+
+ private Class<MODEL> configModelClass;
+
+ public ConfigModelBuilder(Class<MODEL> configModelClass) {
+ this.configModelClass = configModelClass;
+ }
+
+ /**
+ * Method that must return the XML elements this builder handles. Subclasses must implement this in order to
+ * get called when one of the elements have been encountered when parsing.
+ *
+ * @return A list of elements that this builder handles.
+ */
+ public abstract List<ConfigModelId> handlesElements();
+
+ /**
+ * Convenience hook called from {@link #build}. Implement this method to build a config model.
+ *
+ * @param spec The XML element that this builder should handle.
+ * @param modelContext A model context that contains the application package and other data needed by the
+ * config model constructor.
+ */
+ public abstract void doBuild(MODEL model, Element spec, ConfigModelContext modelContext);
+
+ /**
+ * Builds an instance of this component model.
+ * This calls instantiate(...), instance.setUp(...), doBuild(instance, ...).
+ *
+ * @param deployState a global deployment state used for this model.
+ * @param parent the root config producer this should be added to
+ * @param spec the XML element this is constructed from
+ */
+ public final MODEL build(DeployState deployState, ConfigModelRepo configModelRepo, AbstractConfigProducer parent, Element spec) {
+ ConfigModelContext context = ConfigModelContext.create(deployState, configModelRepo, parent, getIdString(spec));
+ return build(new DefaultModelInstanceFactory(), spec, context);
+ }
+
+ /**
+ * Builds an instance of this component model.
+ * This calls instantiate(...), instance.setUp(...), doBuild(instance, ...).
+ *
+ * @param factory A factory capable of creating models.
+ * @param spec the XML element this is constructed from
+ * @param context A context object containing various data used by builders.
+ */
+ public MODEL build(ConfigModelInstanceFactory<MODEL> factory, Element spec, ConfigModelContext context) {
+ MODEL model = factory.createModel(context);
+ doBuild(model, spec, context);
+ return model;
+ }
+
+ public Class<MODEL> getModelClass() {
+ return configModelClass;
+ }
+
+ private static String getIdString(Element spec) {
+ String idString = XmlHelper.getIdString(spec);
+ if (idString == null || idString.isEmpty()) {
+ idString = spec.getTagName();
+ }
+ return idString;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof ConfigModelBuilder)) {
+ return false;
+ }
+ ConfigModelBuilder otherBuilder = (ConfigModelBuilder) other;
+ List<ConfigModelId> thisIds = this.handlesElements();
+ List<ConfigModelId> otherIds = otherBuilder.handlesElements();
+ if (thisIds.size() != otherIds.size()) {
+ return false;
+ }
+ for (int i = 0; i < thisIds.size(); i++) {
+ if (!thisIds.get(i).equals(otherIds.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ private class DefaultModelInstanceFactory implements ConfigModelInstanceFactory<MODEL> {
+ @Override
+ public MODEL createModel(ConfigModelContext context) {
+ try {
+ Constructor<MODEL> constructor = configModelClass.getConstructor(ConfigModelContext.class);
+ return constructor.newInstance(context);
+ } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException("Error constructing model '" + configModelClass.getName() + "'", e);
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelId.java b/config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelId.java
new file mode 100644
index 00000000000..fca114757ec
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/builder/xml/ConfigModelId.java
@@ -0,0 +1,94 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.builder.xml;
+
+import com.yahoo.component.Version;
+
+/**
+ * A {@link ConfigModelId} describes an element handled by a {@link ConfigModelBuilder}.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigModelId implements Comparable<ConfigModelId> {
+
+ private final String name;
+ private final Version version;
+ private final String stringValue;
+
+ private ConfigModelId(String name, Version version) {
+ this.name = name;
+ this.version = version;
+ this.stringValue = toStringValue();
+ }
+
+ /**
+ * Create id with a name and version
+ * @param tagName Name of the id
+ * @param tagVersion Version of the id
+ * @return A ConfigModelId instance
+ */
+ public static ConfigModelId fromNameAndVersion(String tagName, String tagVersion) {
+ return new ConfigModelId(tagName, Version.fromString(tagVersion));
+ }
+
+ /**
+ * Create id with given name, using default version 1.
+ *
+ * @param tagName Name of the id
+ * @return A ConfigModelId instance
+ */
+ public static ConfigModelId fromName(String tagName) {
+ return new ConfigModelId(tagName, new Version(1));
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof ConfigModelId)) return false;
+ ConfigModelId other = (ConfigModelId)object;
+ return this.name.equals(other.name) && this.version.equals(other.version);
+ }
+
+ @Override
+ public int compareTo(ConfigModelId other) {
+ if (other == this) return 0;
+ int cmp = this.name.compareTo(other.name);
+ if (cmp == 0) {
+ cmp = this.version.compareTo(other.version);
+ }
+ return cmp;
+ }
+
+ @Override
+ public String toString() {
+ return stringValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return stringValue.hashCode();
+ }
+
+ /**
+ * Return the XML element name.
+ * @return the name of the config model
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Return the XML element version.
+ * @return the version of the config model
+ */
+ Version getVersion() {
+ return version;
+ }
+
+ private String toStringValue() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(name);
+ sb.append(".");
+ sb.append(version);
+ return sb.toString();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/builder/xml/XmlHelper.java b/config-model/src/main/java/com/yahoo/config/model/builder/xml/XmlHelper.java
new file mode 100644
index 00000000000..924d888b0d0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/builder/xml/XmlHelper.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.builder.xml;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.XML;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Logger;
+
+
+/**
+ * Static methods for helping dom building
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public final class XmlHelper {
+ private static final Logger log = Logger.getLogger(XmlHelper.class.getPackage().toString());
+
+
+ private static final String idReference = "idref";
+ // Access to this needs to be synchronized (as it is in getDocumentBuilder() below)
+ public static final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+
+ static {
+ XmlHelper.factory.setNamespaceAware(true);
+ // if use jdom and jaxen this will fail badly:
+ XmlHelper.factory.setXIncludeAware(true);
+ }
+
+ private XmlHelper() {}
+
+ public static String nullIfEmpty(String attribute) {
+ if (attribute.isEmpty())
+ return null;
+ else
+ return attribute;
+ }
+
+ /**
+ * For searchers inside search chains, the id may be both a reference and an id at once, or just a reference.
+ * In other cases, it is clear which one it is from context, so I think the difference is not worth bothering users
+ * with, unless they are XML purists in which case they will have the option of writing this correctly.
+ * - Jon
+ */
+ public static String getIdString(Element element) {
+ String idString = element.getAttribute("id");
+ if (idString == null || idString.trim().equals(""))
+ idString = element.getAttribute(idReference);
+ if (idString == null || idString.trim().equals(""))
+ idString = element.getAttribute("ident");
+ return idString;
+ }
+
+ public static ComponentId getId(Element element) {
+ return new ComponentId(getIdString(element));
+ }
+
+ public static ComponentSpecification getIdRef(Element element) {
+ return new ComponentSpecification(getIdString(element));
+ }
+
+ public static Document getDocument(Reader reader) {
+ Document doc;
+ try {
+ doc = getDocumentBuilder().parse(new InputSource(reader));
+ } catch (SAXException | IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ return doc;
+ }
+
+ public static List<String> splitAndDiscardEmpty(String field, String regex) {
+ List<String> ret = new ArrayList<>();
+ for (String t : field.split(regex)) {
+ if (!t.isEmpty()) {
+ ret.add(t);
+ }
+ }
+ return ret;
+ }
+
+ public static List<String> spaceSeparatedSymbols(String field) {
+ return splitAndDiscardEmpty(field, " ");
+ }
+
+ public static Collection<String> spaceSeparatedSymbolsFromAttribute(Element spec, String name) {
+ return spaceSeparatedSymbols(spec.getAttribute(name));
+ }
+
+ public static Collection<String> valuesFromElements(Element parent, String elementName) {
+ List<String> symbols = new ArrayList<>();
+ for (Element symbol : XML.getChildren(parent, elementName)) {
+ symbols.add(XML.getValue(symbol).trim());
+ }
+ return symbols;
+ }
+
+ public static boolean isReference(Element element) {
+ return element.hasAttribute(idReference);
+ }
+
+ /**
+ * Creates a new XML document builder.
+ *
+ * @return A new DocumentBuilder instance, or null if we fail to get one.
+ */
+ public static synchronized DocumentBuilder getDocumentBuilder() {
+ try {
+ DocumentBuilder docBuilder = factory.newDocumentBuilder();
+ log.log(LogLevel.DEBUG, "XML parser now operational!");
+ return docBuilder;
+ } catch (ParserConfigurationException e) {
+ log.log(LogLevel.WARNING, "No XML parser available - " + e);
+ return null;
+ }
+ }
+
+ public static Optional<String> getOptionalAttribute(Element element, String name) {
+ return Optional.ofNullable(element.getAttribute(name)).filter(s -> !s.isEmpty());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/builder/xml/package-info.java b/config-model/src/main/java/com/yahoo/config/model/builder/xml/package-info.java
new file mode 100644
index 00000000000..6ce400dc922
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/builder/xml/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.config.model.builder.xml;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/ConfigDefinitionStore.java b/config-model/src/main/java/com/yahoo/config/model/deploy/ConfigDefinitionStore.java
new file mode 100644
index 00000000000..22ea054a27d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/ConfigDefinitionStore.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.deploy;
+
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public interface ConfigDefinitionStore {
+
+ ConfigDefinition getConfigDefinition(ConfigDefinitionKey defKey);
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployProperties.java
new file mode 100644
index 00000000000..3f6ef494a27
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployProperties.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.deploy;
+
+import com.yahoo.config.model.api.ConfigServerSpec;
+import com.yahoo.config.provision.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Collection of properties for a deployment.
+ *
+ * @author lulf
+ * @since 5.17
+ */
+public class DeployProperties {
+ private final boolean multitenant;
+ private final ApplicationId applicationId;
+ private final List<ConfigServerSpec> serverSpecs = new ArrayList<>();
+ private final boolean hostedVespa;
+ private final Version vespaVersion;
+ private final Zone zone;
+
+ private DeployProperties(boolean multitenant,
+ ApplicationId applicationId,
+ List<ConfigServerSpec> configServerSpecs,
+ boolean hostedVespa, Version vespaVersion, Zone zone) {
+ this.vespaVersion = vespaVersion;
+ this.zone = zone;
+ this.multitenant = multitenant || hostedVespa || Boolean.getBoolean("multitenant");
+ this.applicationId = applicationId;
+ this.serverSpecs.addAll(configServerSpecs);
+ this.hostedVespa = hostedVespa;
+ }
+
+
+ public boolean multitenant() {
+ return multitenant;
+ }
+
+ public ApplicationId applicationId() {
+ return applicationId;
+ }
+
+ public List<ConfigServerSpec> configServerSpecs() {
+ return serverSpecs;
+ }
+
+ public Quota quota() {
+ return new Quota(Integer.MAX_VALUE);
+ }
+
+ public boolean hostedVespa() {
+ return hostedVespa;
+ }
+
+ public Version vespaVersion() {
+ return vespaVersion;
+ }
+
+ public Zone zone() {
+ return zone;
+ }
+
+ public static class Builder {
+
+ private ApplicationId applicationId = ApplicationId.defaultId();
+ private boolean multitenant = false;
+ private List<ConfigServerSpec> configServerSpecs = new ArrayList<>();
+ private Quota quota = new Quota(Integer.MAX_VALUE);
+ private boolean hostedVespa = false;
+ private Version vespaVersion = Version.fromIntValues(1, 0, 0);
+ private Zone zone = Zone.defaultZone();
+
+ public Builder applicationId(ApplicationId applicationId) {
+ this.applicationId = applicationId;
+ return this;
+ }
+
+ public Builder multitenant(boolean multitenant) {
+ this.multitenant = multitenant;
+ return this;
+ }
+
+ public Builder configServerSpecs(List<ConfigServerSpec> configServerSpecs) {
+ this.configServerSpecs = configServerSpecs;
+ return this;
+ }
+
+ public Builder quota(Quota quota) {
+ this.quota = quota;
+ return this;
+ }
+
+ public Builder vespaVersion(Version version) {
+ this.vespaVersion = version;
+ return this;
+ }
+
+ public Builder hostedVespa(boolean hostedVespa) {
+ this.hostedVespa = hostedVespa;
+ return this;
+ }
+
+ public Builder zone(Zone zone) {
+ this.zone = zone;
+ return this;
+ }
+
+ public DeployProperties build() {
+ return new DeployProperties(multitenant, applicationId, configServerSpecs, hostedVespa, vespaVersion, zone);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java
new file mode 100644
index 00000000000..5896dc59df2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/DeployState.java
@@ -0,0 +1,381 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.deploy;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.codegen.CNode;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.provision.HostsXmlProvisioner;
+import com.yahoo.config.model.provision.SingleNodeProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.log.LogLevel;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionBuilder;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.xml.ValidationOverridesXMLReader;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import com.yahoo.vespa.model.container.search.QueryProfilesBuilder;
+import com.yahoo.vespa.model.container.search.SemanticRuleBuilder;
+import com.yahoo.vespa.model.container.search.SemanticRules;
+import com.yahoo.vespa.model.search.SearchDefinition;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * Contains various state during deploy that should be reachable to all builders of a {@link com.yahoo.config.model.ConfigModel}
+ *
+ * @author lulf
+ * @since 5.8
+ */
+public class DeployState implements ConfigDefinitionStore {
+
+ private static final Logger log = Logger.getLogger(DeployState.class.getName());
+
+ private final DeployLogger logger;
+ private final FileRegistry fileRegistry;
+ private final DocumentModel documentModel;
+ private final List<SearchDefinition> searchDefinitions;
+ private final ApplicationPackage applicationPackage;
+ private final Optional<ConfigDefinitionRepo> configDefinitionRepo;
+ private final Optional<ApplicationPackage> permanentApplicationPackage;
+ private final Optional<Model> previousModel;
+ private final DeployProperties properties;
+ private final Set<Rotation> rotations;
+ private final Zone zone;
+ private final QueryProfiles queryProfiles;
+ private final SemanticRules semanticRules;
+ private final ValidationOverrides validationOverrides;
+
+ private final HostProvisioner provisioner;
+
+ public static DeployState createTestState() {
+ return new Builder().build();
+ }
+
+ public static DeployState createTestState(ApplicationPackage applicationPackage) {
+ return new Builder().applicationPackage(applicationPackage).build();
+ }
+
+ private DeployState(ApplicationPackage applicationPackage, SearchDocumentModel searchDocumentModel, RankProfileRegistry rankProfileRegistry,
+ FileRegistry fileRegistry, DeployLogger deployLogger, Optional<HostProvisioner> hostProvisioner, DeployProperties properties,
+ Optional<ApplicationPackage> permanentApplicationPackage, Optional<ConfigDefinitionRepo> configDefinitionRepo,
+ java.util.Optional<Model> previousModel, Set<Rotation> rotations, Zone zone, QueryProfiles queryProfiles, SemanticRules semanticRules, Instant now) {
+ this.logger = deployLogger;
+ this.fileRegistry = fileRegistry;
+ this.rankProfileRegistry = rankProfileRegistry;
+ this.applicationPackage = applicationPackage;
+ this.properties = properties;
+ this.previousModel = previousModel;
+ if (hostProvisioner.isPresent()) {
+ this.provisioner = hostProvisioner.get();
+ } else {
+ this.provisioner = getDefaultModelHostProvisioner(applicationPackage);
+ }
+ this.searchDefinitions = searchDocumentModel.getSearchDefinitions();
+ this.documentModel = searchDocumentModel.getDocumentModel();
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ this.configDefinitionRepo = configDefinitionRepo;
+ this.rotations = rotations;
+ this.zone = zone;
+ this.queryProfiles = queryProfiles; // TODO: Remove this by seeing how pagetemplates are propagated
+ this.semanticRules = semanticRules; // TODO: Remove this by seeing how pagetemplates are propagated
+ this.validationOverrides = new ValidationOverridesXMLReader().read(applicationPackage.getValidationOverrides(), now);
+
+ }
+
+ public static HostProvisioner getDefaultModelHostProvisioner(ApplicationPackage applicationPackage) {
+ if (applicationPackage.getHosts() == null) {
+ return new SingleNodeProvisioner();
+ } else {
+ return new HostsXmlProvisioner(applicationPackage.getHosts());
+ }
+ }
+
+ /** Get the global rank profile registry for this application. */
+ public final RankProfileRegistry rankProfileRegistry() { return rankProfileRegistry; }
+
+ /** Returns the validation overrides of this. This is never null */
+ public ValidationOverrides validationOverrides() { return validationOverrides; }
+
+ /**
+ * Returns the config def with the given name and namespace.
+ *
+ * @param defKey The {@link ConfigDefinitionKey} that will uniquely identify a config definition.
+ * @return The definition with a matching name and namespace
+ * @throws java.lang.IllegalArgumentException if def is not found.
+ */
+ public final ConfigDefinition getConfigDefinition(ConfigDefinitionKey defKey) {
+ if (existingConfigDefs == null) {
+ existingConfigDefs = new LinkedHashMap<>();
+ if (configDefinitionRepo.isPresent()) {
+ existingConfigDefs.putAll(createLazyMapping(configDefinitionRepo.get()));
+ }
+ existingConfigDefs.putAll(applicationPackage.getAllExistingConfigDefs());
+ }
+ log.log(LogLevel.DEBUG, "Getting config definition " + defKey);
+ // Fall back to default namespace if not found.
+ if (defKey.getNamespace() == null || defKey.getNamespace().isEmpty()) {
+ defKey = new ConfigDefinitionKey(defKey.getName(), CNode.DEFAULT_NAMESPACE);
+ }
+ ConfigDefinitionKey lookupKey = defKey;
+ // Fall back to just using name
+ if (!existingConfigDefs.containsKey(lookupKey)) {
+
+ int count = 0;
+ for (ConfigDefinitionKey entry : existingConfigDefs.keySet()) {
+ if (entry.getName().equals(defKey.getName())) {
+ count++;
+ }
+ }
+ if (count > 1) {
+ throw new IllegalArgumentException("Using config definition '" + defKey.getName() + "' is ambiguous, there are more than one config definitions with this name, please specify namespace");
+ }
+
+ lookupKey = null;
+ log.log(LogLevel.DEBUG, "Could not find config definition '" + defKey + "', trying with same name in all namespaces");
+ for (ConfigDefinitionKey entry : existingConfigDefs.keySet()) {
+ if (entry.getName().equals(defKey.getName()) && defKey.getNamespace().equals(CNode.DEFAULT_NAMESPACE)) {
+ log.log(LogLevel.INFO, "Could not find config definition '" + defKey + "'" +
+ ", using config definition '" + entry + "' with same name instead (please use new namespace when specifying this config)");
+ lookupKey = entry;
+ break;
+ }
+ }
+ }
+
+ if (lookupKey == null) {
+ throw new IllegalArgumentException("Could not find a config definition with name '" + defKey + "'.");
+ }
+ if (defArchive.get(defKey) != null) {
+ log.log(LogLevel.DEBUG, "Found in archive: " + defKey);
+ return defArchive.get(defKey);
+ }
+
+ log.log(LogLevel.DEBUG, "Retrieving config definition: " + defKey);
+ ConfigDefinition def = existingConfigDefs.get(lookupKey).parse();
+
+ log.log(LogLevel.DEBUG, "Adding " + def + " to archive");
+ defArchive.put(defKey, def);
+ return def;
+ }
+
+ private static Map<ConfigDefinitionKey, UnparsedConfigDefinition> createLazyMapping(final ConfigDefinitionRepo configDefinitionRepo) {
+ Map<ConfigDefinitionKey, UnparsedConfigDefinition> keyToRepo = new LinkedHashMap<>();
+ for (final Map.Entry<ConfigDefinitionKey, com.yahoo.vespa.config.buildergen.ConfigDefinition> defEntry : configDefinitionRepo.getConfigDefinitions().entrySet()) {
+ keyToRepo.put(defEntry.getKey(), new UnparsedConfigDefinition() {
+ @Override
+ public ConfigDefinition parse() {
+ return ConfigDefinitionBuilder.createConfigDefinition(configDefinitionRepo.getConfigDefinitions().get(defEntry.getKey()).getCNode());
+ }
+
+ @Override
+ public String getUnparsedContent() {
+ throw new UnsupportedOperationException("Cannot get unparsed content from " + defEntry.getKey());
+ }
+ });
+ }
+ return keyToRepo;
+ }
+
+ // Global registry of rank profiles.
+ // TODO: I think this can be removed when we remove "<search version=2.0>" and only support content.
+ private final RankProfileRegistry rankProfileRegistry;
+
+ // Mapping from key to something that can create a config definition.
+ private Map<ConfigDefinitionKey, UnparsedConfigDefinition> existingConfigDefs = null;
+
+ // Cache of config defs for all [def,version] combinations looked up so far.
+ private final Map<ConfigDefinitionKey, ConfigDefinition> defArchive = new LinkedHashMap<>();
+
+ public ApplicationPackage getApplicationPackage() {
+ return applicationPackage;
+ }
+ public List<SearchDefinition> getSearchDefinitions() {
+ return searchDefinitions;
+ }
+
+ public DocumentModel getDocumentModel() {
+ return documentModel;
+ }
+
+ public DeployLogger getDeployLogger() {
+ return logger;
+ }
+
+ public FileRegistry getFileRegistry() {
+ return fileRegistry;
+ }
+
+ public HostProvisioner getProvisioner() { return provisioner; }
+
+ public Optional<ApplicationPackage> getPermanentApplicationPackage() {
+ return permanentApplicationPackage;
+ }
+
+ public DeployProperties getProperties() { return properties; }
+
+ public Optional<Model> getPreviousModel() { return previousModel; }
+
+ public boolean isHostedVespa() {
+ return properties.hostedVespa();
+ }
+
+ public Set<Rotation> getRotations() {
+ return this.rotations; // todo: consider returning a copy or immutable view
+ }
+
+ /** Returns the zone in which this is currently running */
+ public Zone zone() { return zone; }
+
+ public QueryProfiles getQueryProfiles() { return queryProfiles; }
+
+ public SemanticRules getSemanticRules() { return semanticRules; }
+
+ public static class Builder {
+
+ private ApplicationPackage applicationPackage = MockApplicationPackage.createEmpty();
+ private FileRegistry fileRegistry = new MockFileRegistry();
+ private DeployLogger logger = new BaseDeployLogger();
+ private Optional<HostProvisioner> hostProvisioner = Optional.empty();
+ private Optional<ApplicationPackage> permanentApplicationPackage = Optional.empty();
+ private DeployProperties properties = new DeployProperties.Builder().build();
+ private Optional<ConfigDefinitionRepo> configDefinitionRepo = Optional.empty();
+ private Optional<Model> previousModel = Optional.empty();
+ private Set<Rotation> rotations = new HashSet<>();
+ private Zone zone = Zone.defaultZone();
+ private Instant now = Instant.now();
+
+ public Builder applicationPackage(ApplicationPackage applicationPackage) {
+ this.applicationPackage = applicationPackage;
+ return this;
+ }
+
+ public Builder fileRegistry(FileRegistry fileRegistry) {
+ this.fileRegistry = fileRegistry;
+ return this;
+ }
+
+ public Builder deployLogger(DeployLogger logger) {
+ this.logger = logger;
+ return this;
+ }
+
+ public Builder modelHostProvisioner(HostProvisioner modelProvisioner) {
+ this.hostProvisioner = Optional.of(modelProvisioner);
+ return this;
+ }
+
+ public Builder permanentApplicationPackage(Optional<ApplicationPackage> permanentApplicationPackage) {
+ this.permanentApplicationPackage = permanentApplicationPackage;
+ return this;
+ }
+
+ public Builder properties(DeployProperties properties) {
+ this.properties = properties;
+ return this;
+ }
+
+ public Builder configDefinitionRepo(ConfigDefinitionRepo configDefinitionRepo) {
+ this.configDefinitionRepo = Optional.of(configDefinitionRepo);
+ return this;
+ }
+
+ public Builder previousModel(Model previousModel) {
+ this.previousModel = Optional.of(previousModel);
+ return this;
+ }
+
+ public Builder rotations(Set<Rotation> rotations) {
+ this.rotations = rotations;
+ return this;
+ }
+
+ public Builder zone(Zone zone) {
+ this.zone = zone;
+ return this;
+ }
+
+ public Builder now(Instant now) {
+ this.now = now;
+ return this;
+ }
+
+ public DeployState build() {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ QueryProfiles queryProfiles = new QueryProfilesBuilder().build(applicationPackage);
+ SemanticRules semanticRules = new SemanticRuleBuilder().build(applicationPackage);
+ SearchDocumentModel searchDocumentModel = createSearchDocumentModel(rankProfileRegistry, logger, queryProfiles);
+ return new DeployState(applicationPackage, searchDocumentModel, rankProfileRegistry, fileRegistry, logger, hostProvisioner,
+ properties, permanentApplicationPackage, configDefinitionRepo, previousModel, rotations, zone, queryProfiles, semanticRules, now);
+ }
+
+ private SearchDocumentModel createSearchDocumentModel(RankProfileRegistry rankProfileRegistry, DeployLogger logger, QueryProfiles queryProfiles) {
+ Collection<NamedReader> readers = applicationPackage.getSearchDefinitions();
+ Map<String, String> names = new LinkedHashMap<>();
+ SearchBuilder builder = new SearchBuilder(applicationPackage, rankProfileRegistry);
+ for (NamedReader reader : readers) {
+ try {
+ String readerName = reader.getName();
+ String searchName = builder.importReader(reader, readerName, logger);
+ String sdName = stripSuffix(readerName, ApplicationPackage.SD_NAME_SUFFIX);
+ names.put(searchName, sdName);
+ if (!sdName.equals(searchName)) {
+ throw new IllegalArgumentException("Search definition file name ('" + sdName + "') and name of " +
+ "search element ('" + searchName + "') are not equal for file '" + readerName + "'");
+ }
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Could not parse search definition file '" + getSearchDefinitionRelativePath(reader.getName()) + "': " + e.getMessage(), e);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Could not read search definition file '" + getSearchDefinitionRelativePath(reader.getName()) + "': " + e.getMessage(), e);
+ } finally {
+ closeIgnoreException(reader.getReader());
+ }
+ }
+ builder.build(logger, queryProfiles);
+ return SearchDocumentModel.fromBuilderAndNames(builder, names);
+ }
+
+ private String getSearchDefinitionRelativePath(String name) {
+ return ApplicationPackage.SEARCH_DEFINITIONS_DIR + File.separator + name;
+ }
+
+ private static String stripSuffix(String nodeName, String postfix) {
+ assert (nodeName.endsWith(postfix));
+ return nodeName.substring(0, nodeName.length() - postfix.length());
+ }
+
+ @SuppressWarnings("EmptyCatchBlock")
+ private static void closeIgnoreException(Reader reader) {
+ try {
+ reader.close();
+ } catch(Exception e) {}
+ }
+ }
+
+}
+
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/SearchDocumentModel.java b/config-model/src/main/java/com/yahoo/config/model/deploy/SearchDocumentModel.java
new file mode 100644
index 00000000000..1fbd72599fe
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/SearchDocumentModel.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.deploy;
+
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import com.yahoo.vespa.model.search.SearchDefinition;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Internal helper class to retrieve document model and search definitions.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class SearchDocumentModel {
+
+ private final DocumentModel documentModel;
+ private final List<SearchDefinition> searchDefinitions;
+
+ public SearchDocumentModel(DocumentModel documentModel, List<SearchDefinition> searchDefinitions) {
+ this.documentModel = documentModel;
+ this.searchDefinitions = searchDefinitions;
+
+ }
+
+ public DocumentModel getDocumentModel() {
+ return documentModel;
+ }
+
+ public List<SearchDefinition> getSearchDefinitions() {
+ return searchDefinitions;
+ }
+
+ public static SearchDocumentModel fromBuilderAndNames(SearchBuilder builder, Map<String, String> names) {
+ List<SearchDefinition> ret = new ArrayList<>();
+ for (com.yahoo.searchdefinition.Search search : builder.getSearchList()) {
+ ret.add(new SearchDefinition(names.get(search.getName()), search));
+ }
+ return new SearchDocumentModel(builder.getModel(), ret);
+ }
+
+ public static SearchDocumentModel fromBuilder(SearchBuilder builder) {
+ List<SearchDefinition> ret = new ArrayList<>();
+ for (com.yahoo.searchdefinition.Search search : builder.getSearchList()) {
+ ret.add(new SearchDefinition(search.getName(), search));
+ }
+ return new SearchDocumentModel(builder.getModel(), ret);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/package-info.java b/config-model/src/main/java/com/yahoo/config/model/deploy/package-info.java
new file mode 100644
index 00000000000..3feb934615f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/deploy/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.config.model.deploy;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/config/model/graph/ModelGraph.java b/config-model/src/main/java/com/yahoo/config/model/graph/ModelGraph.java
new file mode 100644
index 00000000000..a435733e60f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/graph/ModelGraph.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.graph;
+
+import com.yahoo.component.ComponentId;
+
+import java.util.*;
+
+/**
+ * A model graph contains the dependency graph of config models.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ModelGraph {
+
+ private final List<ModelNode> modelNodes;
+ private final List<ModelNode> roots;
+ public ModelGraph(List<ModelNode> modelNodes, List<ModelNode> roots) {
+ this.modelNodes = modelNodes;
+ this.roots = roots;
+ }
+
+ /**
+ * Performs a topological sort ot the models stored in this graph. The algorithm is based on Kahn topological sort.
+ *
+ * @return a sorted list of {@link com.yahoo.config.model.graph.ModelNode} in dependency order.
+ */
+ public List<ModelNode> topologicalSort() {
+ DependencyMap dependencyMap = new DependencyMap(modelNodes);
+ Queue<ModelNode> unprocessed = new LinkedList<>();
+ unprocessed.addAll(roots);
+ List<ModelNode> sortedList = new ArrayList<>();
+ while (!unprocessed.isEmpty()) {
+ ModelNode sortedNode = unprocessed.remove();
+ sortedList.add(sortedNode);
+ for (ModelNode node : modelNodes) {
+ if (dependencyMap.dependsOn(node, sortedNode)) {
+ dependencyMap.removeDependency(node, sortedNode);
+ if (!dependencyMap.hasDependencies(node)) {
+ unprocessed.add(node);
+ }
+ }
+ }
+ }
+ for (ModelNode node : modelNodes) {
+ if (dependencyMap.hasDependencies(node)) {
+ throw new IllegalArgumentException("Unable to sort graph because it contains cycles");
+ }
+ }
+ return sortedList;
+ }
+
+ List<ModelNode> getNodes() {
+ return modelNodes;
+ }
+
+ private static class DependencyMap {
+ private final Map<ComponentId, Set<ComponentId>> map = new LinkedHashMap<>();
+ DependencyMap(List<ModelNode> modelNodes) {
+ for (ModelNode node : modelNodes) {
+ Set<ComponentId> ids = new LinkedHashSet<>();
+ ids.addAll(node.listDependencyIds());
+ map.put(node.id, ids);
+ }
+ }
+
+ public boolean dependsOn(ModelNode node, ModelNode sortedNode) {
+ return map.get(node.id).contains(sortedNode.id);
+ }
+
+ public void removeDependency(ModelNode node, ModelNode sortedNode) {
+ map.get(node.id).remove(sortedNode.id);
+ }
+
+ public boolean hasDependencies(ModelNode node) {
+ return !map.get(node.id).isEmpty();
+ }
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/config/model/graph/ModelGraphBuilder.java b/config-model/src/main/java/com/yahoo/config/model/graph/ModelGraphBuilder.java
new file mode 100644
index 00000000000..97d914c6012
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/graph/ModelGraphBuilder.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.graph;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used to add builders and elements in addBuilder, and then build a dependency graph based on the
+ * constructor arguments.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ModelGraphBuilder {
+
+ private final List<ConfigModelBuilder<? extends ConfigModel>> builders = new ArrayList<>();
+
+ /**
+ * Add a {@link com.yahoo.config.model.builder.xml.ConfigModelBuilder} to this graph.
+ *
+ * @param builder The {@link com.yahoo.config.model.builder.xml.ConfigModelBuilder} to add.
+ * @return this for convenience
+ */
+ public ModelGraphBuilder addBuilder(ConfigModelBuilder<? extends ConfigModel> builder) {
+ builders.add(builder);
+ return this;
+ }
+
+ /**
+ * Build a {@link com.yahoo.config.model.graph.ModelGraph} based on the {@link com.yahoo.config.model.builder.xml.ConfigModelBuilder}s
+ * added to this.
+ *
+ * @return A {@link com.yahoo.config.model.graph.ModelGraph} representing the dependency graph.
+ */
+ public ModelGraph build() {
+ List<ModelNode> modelNodes = new ArrayList<>();
+ for (ConfigModelBuilder<? extends ConfigModel> builder : builders) {
+ modelNodes.add(new ModelNode(builder));
+ }
+ List<ModelNode> roots = new ArrayList<>();
+ for (ModelNode modelNode : modelNodes) {
+ int numDependencies = modelNode.addDependenciesFrom(modelNodes);
+ if (numDependencies == 0) {
+ roots.add(modelNode);
+ }
+ }
+ return new ModelGraph(modelNodes, roots);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/graph/ModelNode.java b/config-model/src/main/java/com/yahoo/config/model/graph/ModelNode.java
new file mode 100644
index 00000000000..0cd045b09c8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/graph/ModelNode.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.graph;
+
+import com.google.inject.Inject;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.ConfigModelInstanceFactory;
+
+import java.lang.reflect.*;
+import java.util.*;
+
+/**
+ * Represents a node in the dependency graph, and contains information about a builders dependencies.
+ * Constructor signatures of model classes must have ConfigModelContext as the first argument and
+ * ConfigModel subclasses or Collection of ConfigModels as subsequent arguments.
+ * Only Collection, not Collection subtypes can be used.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class ModelNode<MODEL extends ConfigModel> implements ConfigModelInstanceFactory<MODEL> {
+
+ final ComponentId id;
+ public final ConfigModelBuilder<MODEL> builder;
+ final Class<MODEL> clazz;
+ final Constructor<MODEL> constructor;
+ final List<MODEL> instances = new ArrayList<>();
+ private final Map<ComponentId, ModelNode> dependencies = new HashMap<>();
+
+ public ModelNode(ConfigModelBuilder<MODEL> builder) {
+ this.id = builder.getId();
+ this.builder = builder;
+ this.clazz = builder.getModelClass();
+ this.constructor = findConstructor(clazz);
+ }
+
+ private Constructor<MODEL> findConstructor(Class<MODEL> clazz) {
+ for (Constructor<?> ctor : clazz.getDeclaredConstructors()) {
+ if (ctor.getAnnotation(Inject.class) != null) {
+ return (Constructor<MODEL>) ctor;
+ }
+ }
+ return (Constructor<MODEL>) clazz.getDeclaredConstructors()[0];
+ }
+
+ boolean hasDependencies() {
+ return !dependencies.isEmpty();
+ }
+
+ boolean dependsOn(ModelNode node) {
+ return dependencies.containsKey(node.id);
+ }
+
+ /**
+ * This adds dependencies base on constructor arguments in the model classes themselves.
+ * These then have to be created by this mini-di framework and then handed to the builders
+ * that will fill them.
+ *
+ * TODO: This should be changed to model dependencies between model builders instead, such
+ * that they can create their model objects, and eventually make them immutable.
+ */
+ int addDependenciesFrom(List<ModelNode> modelNodes) {
+ int numDependencies = 0;
+ for (Type param : constructor.getGenericParameterTypes()) {
+ for (ModelNode node : modelNodes) {
+ if (param.equals(node.clazz) || isCollectionOf(param, node.clazz)) {
+ addDependency(node);
+ numDependencies++;
+ }
+ }
+ }
+ return numDependencies;
+ }
+
+ private boolean isCollectionOf(Type type, Class<?> nodeClazz) {
+ if (type instanceof ParameterizedType) {
+ ParameterizedType t = (ParameterizedType) type;
+ // Note: IntelliJ says the following cannot be equal but that is wrong
+ return (t.getRawType().equals(java.util.Collection.class) && t.getActualTypeArguments().length == 1 && t.getActualTypeArguments()[0].equals(nodeClazz));
+ }
+ return false;
+ }
+
+ private boolean isCollection(Type type) {
+ if (type instanceof ParameterizedType) {
+ ParameterizedType t = (ParameterizedType) type;
+ // Note: IntelliJ says the following cannot be equal but that is wrong
+ return (t.getRawType().equals(java.util.Collection.class) && t.getActualTypeArguments().length == 1);
+ }
+ return false;
+ }
+
+ private void addDependency(ModelNode node) {
+ dependencies.put(node.id, node);
+ }
+
+ Collection<ComponentId> listDependencyIds() {
+ return dependencies.keySet();
+ }
+
+ @Override
+ public MODEL createModel(ConfigModelContext context) {
+ try {
+ Type [] params = constructor.getGenericParameterTypes();
+ if (params.length < 1 || ! params[0].equals(ConfigModelContext.class)) {
+ throw new IllegalArgumentException("Constructor for " + clazz.getName() + " must have as its first argument a " + ConfigModelContext.class.getName());
+ }
+ Object arguments[] = new Object[params.length];
+ arguments[0] = context;
+ for (int i = 1; i < params.length; i++)
+ arguments[i] = findArgument(params[i]);
+ MODEL instance = constructor.newInstance(arguments);
+ instances.add(instance);
+ return instance;
+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException("Error constructing model '" + clazz.getName() + "'", e);
+ }
+ }
+
+ private Object findArgument(Type param) {
+ for (ModelNode dependency : dependencies.values()) {
+ if (param.equals(dependency.clazz))
+ return dependency.instances.get(0);
+ if (isCollectionOf(param, dependency.clazz))
+ return Collections.unmodifiableCollection(dependency.instances);
+ }
+ // For collections, we don't require that dependency has been added, we just give an empty collection
+ if (isCollection(param))
+ return Collections.emptyList();
+ throw new IllegalArgumentException("Unable to find constructor argument " + param + " for " + clazz.getName());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/package-info.java b/config-model/src/main/java/com/yahoo/config/model/package-info.java
new file mode 100644
index 00000000000..594f52c0d14
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.config.model;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducer.java b/config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducer.java
new file mode 100644
index 00000000000..41927bc09a9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducer.java
@@ -0,0 +1,448 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.producer;
+
+import com.google.common.annotations.Beta;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.subscription.ConfigInstanceUtil;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.model.*;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.admin.MonitoringSystem;
+import com.yahoo.vespa.model.utils.FreezableMap;
+
+import java.io.*;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * Superclass for all config producers.
+ * Config producers constructs and returns config instances on request.
+ *
+ * @author gjoranv
+ */
+public abstract class AbstractConfigProducer<CHILD extends AbstractConfigProducer<?>>
+ implements ConfigProducer, ConfigInstance.Producer, Serializable {
+
+ private static final long serialVersionUID = 1L;
+ public static final Logger log = Logger.getLogger(AbstractConfigProducer.class.getPackage().toString());
+ private final String subId;
+ private final boolean hostedVespa;
+ private String configId = null;
+
+ private List<Service> descendantServices = new ArrayList<>();
+
+ private AbstractConfigProducer parent = null;
+
+ private UserConfigRepo userConfigs = new UserConfigRepo();
+
+ private final FreezableMap<String, CHILD> childrenBySubId = new FreezableMap<>(LinkedHashMap.class);
+
+ private static boolean isHostedVespa(AbstractConfigProducer parent) {
+ return (parent != null)
+ && (parent.getRoot() != null)
+ && (parent.getRoot().getDeployState() != null)
+ && parent.getRoot().getDeployState().isHostedVespa();
+ }
+
+ /**
+ * Creates a new AbstractConfigProducer with the given parent and subId.
+ * This constructor will add the resulting producer to the children of parent.
+ *
+ * @param parent The parent of this ConfigProducer
+ * @param subId The fragment of the config id for the producer
+ */
+ public AbstractConfigProducer(AbstractConfigProducer parent, String subId) {
+ this(subId, isHostedVespa(parent));
+
+ if (parent != null) {
+ parent.addChild(this);
+ }
+ }
+
+ protected final void setParent(AbstractConfigProducer parent) { this.parent = parent; }
+ public final String getSubId() { return subId; }
+ public final boolean isHostedVespa() { return hostedVespa; }
+
+ /**
+ * Create an config producer with a configId only. Used e.g. to create root nodes, and producers
+ * that are given children after construction using {@link #addChild(AbstractConfigProducer)}.
+ *
+ * @param subId The sub configId. Note that this can be prefixed when calling addChild with this producer as arg.
+ */
+ public AbstractConfigProducer(String subId) {
+ this(subId, false);
+ }
+
+ private AbstractConfigProducer(String subId, boolean hostedVespa) {
+ if (subId.indexOf('/') != -1) {
+ throw new IllegalArgumentException("A subId might not contain '/' : '" + subId + "'");
+ }
+ this.subId = subId;
+ this.hostedVespa = hostedVespa;
+ }
+
+ /**
+ * Adds a child to this config producer.
+ *
+ * @param child The child config producer to add.
+ */
+ protected void addChild(CHILD child) {
+ if (child == null) {
+ throw new IllegalArgumentException("Trying to add null child for: " + this);
+ }
+ if (child instanceof AbstractConfigProducerRoot) {
+ throw new IllegalArgumentException("Child cannot be a root node: " + child);
+ }
+
+ child.setParent(this);
+ if (childrenBySubId.get(child.getSubId()) != null) {
+ throw new IllegalArgumentException("Multiple services/instances of the id '" + child.getSubId() + "' under the service/instance " +
+ errorMsgClassName() + " '" + subId + "'. (This is commonly caused by service/node index " +
+ "collisions in the config.)." +
+ "\nExisting instance: " + childrenBySubId.get(child.getSubId()) +
+ "\nAttempted to add: " + child);
+ }
+ childrenBySubId.put(child.getSubId(), child);
+
+ if (child instanceof Service) {
+ addDescendantService((Service)child);
+ }
+ }
+
+ public void removeChild(CHILD child) {
+ if (child.getParent() != this)
+ throw new IllegalArgumentException("Could not remove " + child + ": Expected its parent to be " +
+ this + ", but was " + child.getParent());
+
+ if (child instanceof Service)
+ descendantServices.remove(child);
+
+ childrenBySubId.remove(child.getSubId());
+ child.setParent(null);
+ }
+
+ /**
+ * Helper to provide an error message on collisions of sub ids (ignore SimpleConfigProducer, use the parent in that case)
+ */
+ private String errorMsgClassName() {
+ if (getClass().equals(SimpleConfigProducer.class)) return parent.getClass().getSimpleName();
+ return getClass().getSimpleName();
+ }
+
+ /**
+ * Sets the user configs for this producer.
+ *
+ * @param repo User configs repo.
+ */
+ public void setUserConfigs(UserConfigRepo repo) { this.userConfigs = repo; }
+
+ /** Returns the user configs of this */
+ @Override
+ public UserConfigRepo getUserConfigs() { return userConfigs; }
+
+ /**
+ * ConfigProducers that must have a special config id should use
+ * setConfigId() instead of overloading this method. This is
+ * because config IDs must be registered through setConfigId().
+ */
+ public final String getConfigId() {
+ if (configId == null) throw new RuntimeException("The system topology must be frozen first.");
+ return configId;
+ }
+
+ /**
+ * Sets the config id for this producer. Will also add this
+ * service to the root node, so the new config id will be picked
+ * up. Note that this producer will be known with both the old
+ * and the new config id in the root node after using this method.
+ */
+ protected void addConfigId(String id) {
+ if (id == null) throw new NullPointerException("Config ID cannot be null.");
+ getRoot().addDescendant(id, this);
+ if (!isVespa() && (getVespa() != null))
+ getVespa().addDescendant(this);
+ }
+
+ /** Returns this ConfigProducer's children (only 1st level) */
+ public Map<String, CHILD> getChildren() { return Collections.unmodifiableMap(childrenBySubId); }
+
+ @Beta
+ public <J extends AbstractConfigProducer<?>> List<J> getChildrenByTypeRecursive(Class<J> type) {
+ List<J> validChildren = new ArrayList<>();
+
+ if (this.getClass().equals(type)) {
+ validChildren.add(type.cast(this));
+ }
+
+ Map<String, ? extends AbstractConfigProducer<?>> children = this.getChildren();
+ for (AbstractConfigProducer<?> child : children.values()) {
+ validChildren.addAll(child.getChildrenByTypeRecursive(type));
+ }
+
+ return Collections.unmodifiableList(validChildren);
+ }
+
+ /** Returns a list of all the children of this who are instances of Service */
+ public List<Service> getDescendantServices() { return Collections.unmodifiableList(descendantServices); }
+
+ protected void addDescendantService(Service s) { descendantServices.add(s); }
+
+ @Override
+ public final boolean cascadeConfig(ConfigInstance.Builder builder) {
+ boolean found=false;
+ if (parent != null)
+ found = parent.cascadeConfig(builder);
+
+ boolean foundHere = builder.dispatchGetConfig(this);
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "cascadeconfig in " + this + ", getting config " +
+ builder.getClass().getDeclaringClass().getName() + " for config id '" + configId + "' found here=" + foundHere);
+ }
+ found = found || foundHere;
+ return found;
+ }
+
+ @Override
+ public final boolean addUserConfig(ConfigInstance.Builder builder) {
+ boolean didApply = false;
+ if (parent != null) {
+ didApply = parent.addUserConfig(builder);
+ }
+
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "User configs is: " + userConfigs.toString());
+ }
+ // TODO: What do we do with md5. Currently ignored for user configs?
+ ConfigDefinitionKey key = new ConfigDefinitionKey(builder.getDefName(), builder.getDefNamespace());
+ if (userConfigs.get(key) != null) {
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "Apply in " + configId);
+ }
+ applyUserConfig(builder, userConfigs.get(key));
+ didApply = true;
+ }
+ return didApply;
+ }
+
+ private void applyUserConfig(ConfigInstance.Builder builder, ConfigPayloadBuilder payloadBuilder) {
+ ConfigInstance.Builder override;
+ if (builder instanceof GenericConfig.GenericConfigBuilder) {
+ // Means that the builder is unknown and that we should try to apply the payload without
+ // the real builder
+ override = getGenericConfigBuilderOverride((GenericConfig.GenericConfigBuilder) builder, payloadBuilder);
+ } else {
+ override = getConfigInstanceBuilderOverride(builder, ConfigPayload.fromBuilder(payloadBuilder));
+ }
+ ConfigInstanceUtil.setValues(builder, override);
+ }
+
+ private ConfigInstance.Builder getGenericConfigBuilderOverride(GenericConfig.GenericConfigBuilder builder, ConfigPayloadBuilder payloadBuilder) {
+ ConfigDefinitionKey key = new ConfigDefinitionKey(builder.getDefName(), builder.getDefNamespace());
+ return new GenericConfig.GenericConfigBuilder(key, payloadBuilder);
+ }
+
+ private ConfigInstance.Builder getConfigInstanceBuilderOverride(ConfigInstance.Builder builder, ConfigPayload payload) {
+ try {
+ ConfigTransformer transformer = new ConfigTransformer(builder.getClass().getEnclosingClass());
+ return transformer.toConfigBuilder(payload);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Error applying override to builder", e);
+ }
+ }
+
+ /**
+ * Returns the one and only HostSystem of the root node
+ * Must be overridden by root node.
+ */
+ public HostSystem getHostSystem() {
+ return getRoot().getHostSystem();
+ }
+
+ public AbstractConfigProducerRoot getRoot() {
+ return parent == null ? null : parent.getRoot();
+ }
+
+ /**
+ * Returns the {@link ApplicationConfigProducerRoot} that is the parent of this sub-tree, or null
+ * if this sub-tree has no Vespa parent.
+ */
+ private ApplicationConfigProducerRoot getVespa() {
+ if (isRoot()) return null;
+ return isVespa() ? (ApplicationConfigProducerRoot)this : parent.getVespa();
+ }
+
+ private boolean isRoot() {
+ return parent == null;
+ }
+
+ private boolean isVespa() {
+ return ((this instanceof ApplicationConfigProducerRoot) && parent.isRoot());
+ }
+
+ public AbstractConfigProducer getParent() { return parent; }
+
+ /**
+ * Writes files that need to be written. The files will usually only be
+ * written when the Vespa model is generated through the deploy-application
+ * script.
+ *
+ * TODO: Make sure all implemented ConfigProducers call createConfig()
+ * instead of getConfig() when implementing this method.
+ */
+ public void writeFiles(File directory) throws java.io.IOException {
+ if (!directory.isDirectory() && !directory.mkdirs()) {
+ throw new java.io.IOException("Cannot create directory: "+ directory);
+ }
+ for (Method m : getClass().getMethods()) {
+ try {
+ ConfigInstance.Builder builder = getBuilderIfIsGetConfig(m);
+ if (builder!=null) {
+ writeBuilder(directory, m, builder);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void writeBuilder(File directory, Method m,
+ ConfigInstance.Builder builder) throws IllegalAccessException,
+ InvocationTargetException, InstantiationException,
+ NoSuchMethodException, IOException {
+ m.invoke(this, builder);
+ Class<?> configInstClass = builder.getClass().getEnclosingClass();
+ ConfigInstance inst = (ConfigInstance) configInstClass.getConstructor(builder.getClass()).newInstance(builder);
+ List<String> payloadList = ConfigInstance.serialize(inst);
+ File outfn = new File(directory, ConfigInstance.getDefName(inst.getClass()) + ".MODEL.cfg");
+ FileOutputStream out = new FileOutputStream(outfn);
+ for (String s : payloadList) {
+ out.write(Utf8.toBytes(s));
+ out.write('\n');
+ }
+ }
+
+ /**
+ * New Builder instance if m is getConfig(SomeConfig.Builder), or null
+ */
+ private ConfigInstance.Builder getBuilderIfIsGetConfig(Method m) throws InstantiationException, IllegalAccessException {
+ if (!"getConfig".equals(m.getName())) return null;
+ Type[] params = m.getParameterTypes();
+ if (params.length!=1) return null;
+ Type param = params[0];
+ if (!(param instanceof Class)) return null;
+ Class<?> paramClass = (Class<?>) param;
+ if (!(ConfigInstance.Builder.class.isAssignableFrom(paramClass))) return null;
+ return (ConfigInstance.Builder) paramClass.newInstance();
+ }
+
+ public void dump(PrintStream out) {
+ for (ConfigProducer c : getChildren().values()) {
+ out.println("id: " + c.getConfigId());
+ if (c.getChildren().size() > 0) {
+ c.dump(out);
+ }
+ }
+ }
+
+ void setupConfigId(String parentConfigId) {
+ if (this instanceof AbstractConfigProducerRoot) {
+ configId = "";
+ } else {
+ configId = parentConfigId + subId;
+ addConfigId(configId);
+ }
+
+ if (this instanceof AbstractConfigProducerRoot || this instanceof ApplicationConfigProducerRoot) {
+ setupChildConfigIds("");
+ } else {
+ setupChildConfigIds(configId + '/');
+ }
+ }
+
+ private static ClassLoader findInheritedClassLoader(Class clazz, String producerName) {
+ Class<?>[] interfazes = clazz.getInterfaces();
+ for (Class interfaze : interfazes) {
+ if (producerName.equals(interfaze.getName())) {
+ return interfaze.getClassLoader();
+ }
+ }
+ if (clazz.getSuperclass() == null)
+ return null;
+ return findInheritedClassLoader(clazz.getSuperclass(), producerName);
+ }
+
+ public ClassLoader getConfigClassLoader(String producerName) {
+ ClassLoader classLoader = findInheritedClassLoader(getClass(), producerName);
+ if (classLoader != null)
+ return classLoader;
+
+ // TODO: Make logic correct, so that the deepest child will be the one winning.
+ for (AbstractConfigProducer child : childrenBySubId.values()) {
+ ClassLoader loader = child.getConfigClassLoader(producerName);
+ if (loader != null) {
+ return loader;
+ }
+ }
+ return null;
+ }
+
+ private void setupChildConfigIds(String currentConfigId) {
+ for (AbstractConfigProducer child : childrenBySubId.values()) {
+ child.setupConfigId(currentConfigId);
+ }
+ }
+
+ void aggregateDescendantServices() {
+ for (AbstractConfigProducer child : childrenBySubId.values()) {
+ child.aggregateDescendantServices();
+ descendantServices.addAll(child.descendantServices);
+ }
+ }
+
+ void freeze() {
+ childrenBySubId.freeze();
+ for (AbstractConfigProducer child : childrenBySubId.values()) {
+ child.freeze();
+ }
+ }
+
+ public void mergeUserConfigs(UserConfigRepo newRepo) {
+ userConfigs.merge(newRepo);
+ }
+
+ @Override
+ public void validate() throws Exception {
+ assert (childrenBySubId.isFrozen());
+
+ for (AbstractConfigProducer child : childrenBySubId.values()) {
+ child.validate();
+ }
+ }
+
+ /** Returns a logger to be used for warnings and messages during initialization, never null */
+ public DeployLogger deployLogger() {
+ return parent.deployLogger();
+ }
+
+ // TODO: Make producers depend on AdminModel instead
+ /** Returns a monitoring service (yamas if that is configured, null otherwise) */
+ protected MonitoringSystem getMonitoringService() {
+ AbstractConfigProducerRoot root = getRoot();
+ Admin admin = (root == null? null : root.getAdmin());
+ if (admin == null) {
+ return null;
+ }
+ if (admin.getYamas() != null) {
+ return admin.getYamas();
+ }
+ return null;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducerRoot.java b/config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducerRoot.java
new file mode 100644
index 00000000000..b85e2b8fa97
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/producer/AbstractConfigProducerRoot.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.producer;
+
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.vespa.model.ConfigProducer;
+import com.yahoo.vespa.model.ConfigProducerRoot;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.filedistribution.FileDistributionConfigProducer;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * The parent class of classes having the role as the root of a config producer tree.
+ *
+ * @author tonytv
+ */
+public abstract class AbstractConfigProducerRoot extends AbstractConfigProducer<AbstractConfigProducer<?>>
+ implements ConfigProducerRoot {
+
+ /** The ConfigProducers contained in this model indexed by config id */
+ protected final Map<String, ConfigProducer> id2producer = new LinkedHashMap<>();
+
+ public AbstractConfigProducerRoot(String rootConfigId) {
+ super(rootConfigId);
+ }
+
+ public AbstractConfigProducerRoot getRoot() {
+ return this;
+ }
+
+ public abstract FileDistributionConfigProducer getFileDistributionConfigProducer();
+
+ /**
+ * Freezes the parent - child connections of the model
+ * and sets information derived from the topology.
+ */
+ public void freezeModelTopology() {
+ freeze();
+ setupConfigId("");
+ aggregateDescendantServices();
+ }
+
+ public abstract ConfigModelRepo configModelRepo();
+
+ /**
+ * Returns the ConfigProducer with the given id if such configId exists.
+ *
+ * @param configId The configId, e.g. "search.0/tld.0"
+ * @return ConfigProducer with the given configId
+ */
+ public Optional<ConfigProducer> getConfigProducer(String configId) {
+ return Optional.ofNullable(id2producer.get(configId));
+ }
+
+ /**
+ * Returns the Service with the given id if such configId exists and it belongs to a Service ConfigProducer.
+ *
+ * @param configId The configId, e.g. "search.0/tld.0"
+ * @return Service with the given configId
+ */
+ public Optional<Service> getService(String configId) {
+ return getConfigProducer(configId)
+ .filter(Service.class::isInstance)
+ .map(Service.class::cast);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/producer/UserConfigRepo.java b/config-model/src/main/java/com/yahoo/config/model/producer/UserConfigRepo.java
new file mode 100644
index 00000000000..d32b0c75e69
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/producer/UserConfigRepo.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.producer;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.*;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A UserConfigRepo is a repository for user configs, typically for a particular config producer. The repo encapsulates
+ * how the user configs are stored, and defines the methods to retrieve user configs and merge the repo with others.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class UserConfigRepo {
+ private final Map<ConfigDefinitionKey, ConfigPayloadBuilder> userConfigsMap;
+
+ public UserConfigRepo() {
+ this.userConfigsMap = new LinkedHashMap<>();
+ }
+
+ @Override
+ public UserConfigRepo clone() {
+ return new UserConfigRepo(copyBuilders(userConfigsMap));
+ }
+
+ /**
+ * Must copy the builder, because the merge method on {@link AbstractConfigProducer} might override the row's builders otherwise
+ */
+ private Map<ConfigDefinitionKey, ConfigPayloadBuilder> copyBuilders(Map<ConfigDefinitionKey, ConfigPayloadBuilder> source) {
+ Map<ConfigDefinitionKey, ConfigPayloadBuilder> ret = new LinkedHashMap<>();
+ for (Map.Entry<ConfigDefinitionKey, ConfigPayloadBuilder> e : source.entrySet()) {
+ ConfigDefinitionKey key = e.getKey();
+ ConfigPayloadBuilder sourceVal = e.getValue();
+ ConfigPayloadBuilder destVal = new ConfigPayloadBuilder(ConfigPayload.fromBuilder(sourceVal));
+ ret.put(key, destVal);
+ }
+ return ret;
+ }
+
+ public UserConfigRepo(Map<ConfigDefinitionKey, ConfigPayloadBuilder> map) {
+ this.userConfigsMap = map;
+ }
+
+ public UserConfigRepo(UserConfigRepo userConfigRepo) {
+ this.userConfigsMap = userConfigRepo.userConfigsMap;
+ }
+
+ public ConfigPayloadBuilder get(ConfigDefinitionKey key) {
+ return userConfigsMap.get(key);
+ }
+
+ public void merge(UserConfigRepo newRepo) {
+ for (Map.Entry<ConfigDefinitionKey, ConfigPayloadBuilder> entry : newRepo.userConfigsMap.entrySet()) {
+ if (entry.getValue() == null) continue;
+
+ ConfigDefinitionKey key = entry.getKey();
+ if (userConfigsMap.containsKey(key)) {
+ ConfigPayloadBuilder lhsBuilder = userConfigsMap.get(key);
+ ConfigPayloadBuilder rhsBuilder = entry.getValue();
+ lhsBuilder.override(rhsBuilder);
+ } else {
+ userConfigsMap.put(key, entry.getValue());
+ }
+ }
+ }
+
+ public boolean isEmpty() {
+ return userConfigsMap.isEmpty();
+ }
+
+ public int size() {
+ return userConfigsMap.size();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (ConfigDefinitionKey key : userConfigsMap.keySet()) {
+ sb.append(key.toString());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * The keys of all the configs contained in this.
+ * @return a set of ConfigDefinitionsKey
+ */
+ public Set<ConfigDefinitionKey> configsProduced() {
+ return userConfigsMap.keySet();
+ }
+
+ /**
+ * Will take the warning messages stored on the payload builders, and apply them to the producer's {@link DeployLogger}
+ * @param producer the producer to apply warnings to
+ */
+ public void applyWarnings(AbstractConfigProducer<?> producer) {
+ for (ConfigPayloadBuilder b : userConfigsMap.values()) {
+ for (String warning : b.warnings()) {
+ producer.deployLogger().log(LogLevel.WARNING, warning);
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/producer/package-info.java b/config-model/src/main/java/com/yahoo/config/model/producer/package-info.java
new file mode 100644
index 00000000000..c58a116b196
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/producer/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.config.model.producer;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/Host.java b/config-model/src/main/java/com/yahoo/config/model/provision/Host.java
new file mode 100644
index 00000000000..9d97308fc76
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/provision/Host.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A hostname with zero or more aliases.
+ *
+ * @author musum
+ */
+public class Host {
+
+ private final String hostname;
+ private final List<String> hostAliases;
+
+ public Host(String hostname) {
+ this.hostname = hostname;
+ this.hostAliases = new ArrayList<>();
+ }
+
+ public Host(String hostname, List<String> hostAliases) {
+ this.hostname = hostname;
+ this.hostAliases = hostAliases;
+ }
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public List<String> getHostAliases() {
+ return hostAliases;
+ }
+
+ @Override
+ public String toString() {
+ return hostname + " (aliases: " + hostAliases + ")";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/Hosts.java b/config-model/src/main/java/com/yahoo/config/model/provision/Hosts.java
new file mode 100644
index 00000000000..ece781dd5bd
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/provision/Hosts.java
@@ -0,0 +1,120 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.log.LogLevel;
+import com.yahoo.net.HostName;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * TODO: What is this?
+ *
+ * @author musum
+ */
+public class Hosts {
+
+ public static final Logger log = Logger.getLogger(Hosts.class.getPackage().toString());
+
+ private final HashMap<String, Host> hosts = new LinkedHashMap<>();
+ private final Map<String, String> alias2hostname = new LinkedHashMap<>();
+ private final Map<String, Host> alias2host = new LinkedHashMap<>();
+
+ /**
+ * Builds host system from a hosts.xml file
+ *
+ * @param hostsFile a reader for host from application package
+ * @return the HostSystem for this application package
+ */
+ public static Hosts getHosts(Reader hostsFile) {
+ Hosts hosts = new Hosts();
+ Document doc;
+ try {
+ doc = XmlHelper.getDocumentBuilder().parse(new InputSource(hostsFile));
+ } catch (SAXException | IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ for (Element hostE : XML.getChildren(doc.getDocumentElement(), "host")) {
+ String name = hostE.getAttribute("name");
+ if (name.equals("")) {
+ throw new RuntimeException("Missing 'name' attribute for host.");
+ }
+ if ("localhost".equals(name)) {
+ name = HostName.getLocalhost();
+ }
+ final List<String> hostAliases = VespaDomBuilder.getHostAliases(hostE.getChildNodes());
+ if (hostAliases.isEmpty()) {
+ throw new IllegalArgumentException("No host aliases defined for host '" + name + "'");
+ }
+ Host host = new Host(name, hostAliases);
+ hosts.addHost(host, hostAliases);
+ }
+ log.log(LogLevel.DEBUG, "Created hosts:" + hosts);
+ return hosts;
+ }
+
+ public Collection<Host> getHosts() {
+ return hosts.values();
+ }
+
+ /**
+ * Adds one host to this host system.
+ *
+ * @param host The host to add
+ * @param aliases The aliases for this host.
+ */
+ public void addHost(Host host, List<String> aliases) {
+ hosts.put(host.getHostname(), host);
+ if ((aliases != null) && (aliases.size() > 0)) {
+ addHostAliases(aliases, host);
+ }
+ }
+
+ /**
+ * Add all aliases for one host
+ *
+ * @param hostAliases A list of host aliases
+ * @param host The Host instance to add the alias for
+ */
+ private void addHostAliases(List<String> hostAliases, Host host) {
+ if (hostAliases.size() < 1) {
+ throw new RuntimeException("Host '" + host.getHostname() + "' must have at least one <alias> tag.");
+ }
+ for (String alias : hostAliases) {
+ addHostAlias(alias, host);
+ }
+ }
+
+ /**
+ * Adds an alias for the given host
+ *
+ * @param alias alias (string) for a Host
+ * @param host the {@link Host} to add the alias for
+ */
+ protected void addHostAlias(String alias, Host host) {
+ if (alias2hostname.containsKey(alias)) {
+ throw new RuntimeException("Alias '" + alias + "' must be used for only one host!");
+ }
+ alias2hostname.put(alias, host.getHostname());
+ alias2host.put(alias, host);
+ }
+
+ public Map<String, Host> getAlias2host() {
+ return alias2host;
+ }
+
+ @Override
+ public String toString() {
+ return "Hosts: " + hosts.keySet();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/HostsXmlProvisioner.java b/config-model/src/main/java/com/yahoo/config/model/provision/HostsXmlProvisioner.java
new file mode 100644
index 00000000000..20dc190d8e7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/provision/HostsXmlProvisioner.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.provision.*;
+import com.yahoo.vespa.model.container.Container;
+
+import java.io.Reader;
+import java.util.List;
+
+/**
+ * A host provisioner based on a hosts.xml file.
+ * No state in this provisioner, i.e it does not know anything about the active
+ * application if one exists. Pre-condition: A valid hosts file.
+ *
+ * @author musum
+ * @since 5.11
+ */
+public class HostsXmlProvisioner implements HostProvisioner {
+
+ private final Hosts hosts;
+ public static final String IMPLICIT_ADMIN_HOSTALIAS = "INTERNAL_VESPA_IMPLICIT_ADMIN";
+
+ public HostsXmlProvisioner(Reader hosts) {
+ this.hosts = Hosts.getHosts(hosts);
+ }
+
+ @Override
+ public HostSpec allocateHost(String alias) {
+ /**
+ * Some special rules to allow no admin elements as well
+ * as jdisc element without nodes.
+ */
+ if (alias.equals(IMPLICIT_ADMIN_HOSTALIAS)) {
+ if (hosts.getHosts().size() > 1) {
+ throw new IllegalArgumentException("More than 1 host specified (" + hosts.getHosts().size() + ") and <admin> not specified");
+ } else {
+ return host2HostSpec(getFirstHost());
+ }
+ } else if (alias.equals(Container.SINGLENODE_CONTAINER_SERVICESPEC)) {
+ return host2HostSpec(getFirstHost());
+ }
+ for (Host host : hosts.getHosts()) {
+ if (host.getHostAliases().contains(alias)) {
+ return new HostSpec(host.getHostname(), host.getHostAliases());
+ }
+ }
+ throw new IllegalArgumentException("Unable to find host for alias '" + alias + "'");
+ }
+
+ @Override
+ public List<HostSpec> prepare(ClusterSpec cluster, Capacity quantity, int groups, ProvisionLogger logger) {
+ throw new UnsupportedOperationException();
+ }
+
+ private HostSpec host2HostSpec(Host host) {
+ return new HostSpec(host.getHostname(), host.getHostAliases());
+ }
+
+ private Host getFirstHost() {
+ return hosts.getHosts().iterator().next();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java
new file mode 100644
index 00000000000..c06efece329
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/provision/InMemoryProvisioner.java
@@ -0,0 +1,164 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import com.yahoo.collections.ListMap;
+import com.yahoo.collections.Pair;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.provision.*;
+
+import java.util.*;
+
+/**
+ * In memory host provisioner. NB! ATM cannot be reused after allocate has been called.
+ *
+ * @author musum
+ * @author bratseth
+ */
+public class InMemoryProvisioner implements HostProvisioner {
+
+ /**
+ * If this is true an exception is thrown when all nodes are used.
+ * If false this will simply return nodes best effort, preferring to satisfy the
+ * number of groups requested when possible.
+ */
+ private final boolean failOnOutOfCapacity;
+
+ /** Hosts which should be returned as retired */
+ private final Set<String> retiredHostNames;
+
+ /** Free hosts of each flavor */
+ private final ListMap<String, Host> freeNodes = new ListMap<>();
+ private final Map<String, HostSpec> legacyMapping = new LinkedHashMap<>();
+ private final Map<ClusterSpec, List<HostSpec>> allocations = new LinkedHashMap<>();
+
+ /** Indexes must be unique across all groups in a cluster */
+ private final Map<Pair<ClusterSpec.Type,ClusterSpec.Id>, Integer> nextIndexInCluster = new HashMap<>();
+
+ /** Use this index as start index for all clusters */
+ private final int startIndexForClusters;
+
+ /** Creates this with a number of nodes of the flavor 'default' */
+ public InMemoryProvisioner(int nodeCount) {
+ this(Collections.singletonMap("default", createHostInstances(nodeCount)), true, 0);
+ }
+
+ /** Creates this with a set of host names of the flavor 'default' */
+ public InMemoryProvisioner(boolean failOnOutOfCapacity, String... hosts) {
+ this(Collections.singletonMap("default", toHostInstances(hosts)), failOnOutOfCapacity, 0);
+ }
+
+ /** Creates this with a set of hosts of the flavor 'default' */
+ public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, String ... retiredHostNames) {
+ this(Collections.singletonMap("default", hosts.getHosts()), failOnOutOfCapacity, 0, retiredHostNames);
+ }
+
+ /** Creates this with a set of hosts of the flavor 'default' */
+ public InMemoryProvisioner(Hosts hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) {
+ this(Collections.singletonMap("default", hosts.getHosts()), failOnOutOfCapacity, startIndexForClusters, retiredHostNames);
+ }
+
+ public InMemoryProvisioner(Map<String, Collection<Host>> hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) {
+ this.failOnOutOfCapacity = failOnOutOfCapacity;
+ for (Map.Entry<String, Collection<Host>> hostsOfFlavor : hosts.entrySet())
+ for (Host host : hostsOfFlavor.getValue())
+ freeNodes.put(hostsOfFlavor.getKey(), host);
+ this.retiredHostNames = new HashSet<>(Arrays.asList(retiredHostNames));
+ this.startIndexForClusters = startIndexForClusters;
+ }
+
+ private static Collection<Host> toHostInstances(String[] hostnames) {
+ List<Host> hosts = new ArrayList<>();
+ for (String hostname : hostnames) {
+ hosts.add(new Host(hostname));
+ }
+ return hosts;
+ }
+
+ private static Collection<Host> createHostInstances(int hostCount) {
+ List<Host> hosts = new ArrayList<>();
+ for (int i = 1; i <= hostCount; i++) {
+ hosts.add(new Host("host" + i));
+ }
+ return hosts;
+ }
+
+ @Override
+ public HostSpec allocateHost(String alias) {
+ if (legacyMapping.containsKey(alias)) return legacyMapping.get(alias);
+ List<Host> defaultHosts = freeNodes.get("default");
+ if (defaultHosts.isEmpty()) throw new IllegalArgumentException("No more hosts of default flavor available");
+ Host newHost = freeNodes.removeValue("default", 0);
+ HostSpec hostSpec = new HostSpec(newHost.getHostname(), newHost.getHostAliases());
+ legacyMapping.put(alias, hostSpec);
+ return hostSpec;
+ }
+
+ @Override
+ public List<HostSpec> prepare(ClusterSpec cluster, Capacity requestedCapacity, int groups, ProvisionLogger logger) {
+ if (cluster.group().isPresent() && groups > 1)
+ throw new IllegalArgumentException("Cannot both be specifying a group and ask for groups to be created");
+ if (requestedCapacity.nodeCount() % groups != 0)
+ throw new IllegalArgumentException("Requested " + requestedCapacity.nodeCount() + " nodes in " +
+ groups + " groups, but the node count is not divisible into this number of groups");
+
+ int capacity = failOnOutOfCapacity ? requestedCapacity.nodeCount() :
+ Math.min(requestedCapacity.nodeCount(), freeNodes.get("default").size() + totalAllocatedTo(cluster));
+ if (groups > capacity)
+ groups = capacity;
+
+ String flavor = requestedCapacity.flavor().orElse("default");
+
+ List<HostSpec> allocation = new ArrayList<>();
+ if (groups == 1) {
+ allocation.addAll(allocateHostGroup(cluster, flavor, capacity, startIndexForClusters));
+ }
+ else {
+ for (int i = 0; i < groups; i++) {
+ allocation.addAll(allocateHostGroup(cluster.changeGroup(Optional.of(ClusterSpec.Group.from(String.valueOf(i)))),
+ flavor,
+ capacity / groups,
+ allocation.size()));
+ }
+ }
+ for (ListIterator<HostSpec> i = allocation.listIterator(); i.hasNext(); ) {
+ HostSpec host = i.next();
+ if (retiredHostNames.contains(host.hostname()))
+ i.set(retire(host));
+ }
+ return allocation;
+ }
+
+ private HostSpec retire(HostSpec host) {
+ return new HostSpec(host.hostname(), host.aliases(), host.membership().get().retire());
+ }
+
+ private List<HostSpec> allocateHostGroup(ClusterSpec clusterGroup, String flavor, int nodesInGroup, int startIndex) {
+ List<HostSpec> allocation = allocations.getOrDefault(clusterGroup, new ArrayList<>());
+ allocations.put(clusterGroup, allocation);
+
+ int nextIndex = nextIndexInCluster.getOrDefault(new Pair<>(clusterGroup.type(), clusterGroup.id()), startIndex);
+ while (allocation.size() < nodesInGroup) {
+ if (freeNodes.get(flavor).isEmpty()) throw new IllegalArgumentException("No nodes of flavor '" + flavor + "' available");
+ Host newHost = freeNodes.removeValue(flavor, 0);
+ ClusterMembership membership = ClusterMembership.from(clusterGroup, nextIndex++);
+ allocation.add(new HostSpec(newHost.getHostname(), newHost.getHostAliases(), membership));
+ }
+ nextIndexInCluster.put(new Pair<>(clusterGroup.type(), clusterGroup.id()), nextIndex);
+
+ while (allocation.size() > nodesInGroup)
+ allocation.remove(0);
+
+ return allocation;
+ }
+
+ private int totalAllocatedTo(ClusterSpec cluster) {
+ int count = 0;
+ for (Map.Entry<ClusterSpec, List<HostSpec>> allocation : allocations.entrySet()) {
+ if ( ! allocation.getKey().type().equals(cluster.type())) continue;
+ if ( ! allocation.getKey().id().equals(cluster.id())) continue;
+ count += allocation.getValue().size();
+ }
+ return count;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/SingleNodeProvisioner.java b/config-model/src/main/java/com/yahoo/config/model/provision/SingleNodeProvisioner.java
new file mode 100644
index 00000000000..38ed728e4e8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/provision/SingleNodeProvisioner.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.provision.*;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.model.HostSystem;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.net.UnknownHostException;
+
+/**
+ * A host provisioner used when there is no hosts.xml file (using localhost as the only host)
+ * No state in this provisioner, i.e it does not know anything about the active
+ * application if one exists.
+ *
+ * @author musum
+ * @since 5.11
+ */
+public class SingleNodeProvisioner implements HostProvisioner {
+
+ private final Host host; // the only host in this system
+ private final HostSpec hostSpec;
+ private int counter = 0;
+
+ public SingleNodeProvisioner() {
+ try {
+ host = new Host(HostSystem.lookupCanonicalHostname(HostName.getLocalhost()));
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ this.hostSpec = new HostSpec(host.getHostname(), host.getHostAliases());
+ }
+
+ @Override
+ public HostSpec allocateHost(String alias) {
+ return hostSpec;
+ }
+
+ @Override
+ public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) { // TODO: This should fail if capacity requested is more than 1
+ List<HostSpec> hosts = new ArrayList<>();
+ hosts.add(new HostSpec(host.getHostname(), host.getHostAliases(), ClusterMembership.from(cluster, counter++)));
+ return hosts;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/provision/package-info.java b/config-model/src/main/java/com/yahoo/config/model/provision/package-info.java
new file mode 100644
index 00000000000..a39e4025efa
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/provision/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.config.model.provision;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/config/model/test/ConfigModelTestUtil.java b/config-model/src/main/java/com/yahoo/config/model/test/ConfigModelTestUtil.java
new file mode 100644
index 00000000000..b75147774ab
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/test/ConfigModelTestUtil.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.test;
+
+import com.yahoo.collections.CollectionUtil;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigModelTestUtil {
+ /**
+ * @param xmlLines XML with " replaced with '
+ */
+ public static Element parse(String... xmlLines) {
+ List<String> lines = new ArrayList<>();
+ lines.add("<?xml version='1.0' encoding='utf-8' ?>");
+ lines.addAll(Arrays.asList(xmlLines));
+
+ try {
+ return XmlHelper.getDocumentBuilder().parse(
+ inputSource((CollectionUtil.mkString(lines, "\n").replace("'", "\""))))
+ .getDocumentElement();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static InputSource inputSource(String str) {
+ return new InputSource(new StringReader(str));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java b/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java
new file mode 100644
index 00000000000..731410c9bf3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java
@@ -0,0 +1,243 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.test;
+
+import com.yahoo.config.application.api.ComponentInfo;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.provision.Version;
+import com.yahoo.io.IOUtils;
+import com.yahoo.path.Path;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.searchdefinition.*;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.config.application.api.ApplicationPackage;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.*;
+
+/**
+ * For testing purposes only
+ *
+ * @author tonytv
+ */
+public class MockApplicationPackage implements ApplicationPackage {
+ public static final String MUSIC_SEARCHDEFINITION = createSearchDefinition("music", "foo");
+ public static final String BOOK_SEARCHDEFINITION = createSearchDefinition("book", "bar");
+
+ private final String hostsS;
+ private final String servicesS;
+ private final List<String> searchDefinitions;
+ private final String searchDefinitionDir;
+ private final Optional<String> deploymentInfo;
+ private final Optional<String> validationOverrides;
+ private final boolean failOnValidateXml;
+
+ private MockApplicationPackage(String hosts, String services, List<String> searchDefinitions, String searchDefinitionDir,
+ String deploymentInfo, String validationOverrides, boolean failOnValidateXml) {
+ this.hostsS = hosts;
+ this.servicesS = services;
+ this.searchDefinitions = searchDefinitions;
+ this.searchDefinitionDir = searchDefinitionDir;
+ this.deploymentInfo = Optional.ofNullable(deploymentInfo);
+ this.validationOverrides = Optional.ofNullable(validationOverrides);
+ this.failOnValidateXml = failOnValidateXml;
+ }
+
+ @Override
+ public String getApplicationName() {
+ return "mock application";
+ }
+
+ @Override
+ public Reader getServices() {
+ return new StringReader(servicesS);
+ }
+
+ @Override
+ public Reader getHosts() {
+ if (hostsS==null) return null;
+ return new StringReader(hostsS);
+ }
+
+ @Override
+ public List<NamedReader> getSearchDefinitions() {
+ ArrayList<NamedReader> readers = new ArrayList<>();
+ SearchBuilder searchBuilder = new SearchBuilder(this, new RankProfileRegistry());
+ for (String sd : searchDefinitions) {
+ try {
+ String name = searchBuilder.importString(sd);
+ readers.add(new NamedReader(name + ApplicationPackage.SD_NAME_SUFFIX, new StringReader(sd)));
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return readers;
+ }
+
+ @Override
+ public List<NamedReader> searchDefinitionContents() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public Map<ConfigDefinitionKey, UnparsedConfigDefinition> getAllExistingConfigDefs() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public List<NamedReader> getFiles(Path dir,String fileSuffix,boolean recurse) {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public ApplicationFile getFile(Path file) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getHostSource() {
+ return "mock source";
+ }
+
+ @Override
+ public String getServicesSource() {
+ return "mock source";
+ }
+
+ @Override
+ public Optional<Reader> getDeployment() {
+ return deploymentInfo.map(StringReader::new);
+ }
+
+ @Override
+ public Optional<Reader> getValidationOverrides() {
+ return validationOverrides.map(StringReader::new);
+ }
+
+ public List<ComponentInfo> getComponentsInfo(Version vespaVersion) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Reader getRankingExpression(String name) {
+ File expressionFile = new File(searchDefinitionDir, name);
+ try {
+ return IOUtils.createReader(expressionFile, "utf-8");
+ }
+ catch (IOException e) {
+ throw new IllegalArgumentException("Could not read ranking expression file '" +
+ expressionFile.getAbsolutePath() + "'", e);
+ }
+ }
+
+ public static ApplicationPackage createEmpty() {
+ return new MockApplicationPackage.Builder().withHosts(emptyHosts).withServices(emptyServices).build();
+ }
+
+ public static ApplicationPackage fromSearchDefinitionDirectory(String dir) {
+ return new MockApplicationPackage.Builder()
+ .withEmptyHosts()
+ .withEmptyServices()
+ .withSearchDefinitionDir(dir).build();
+ }
+
+ public static class Builder {
+ private String hosts = null;
+ private String services = null;
+ private List<String> searchDefinitions = Collections.emptyList();
+ private String searchDefinitionDir = null;
+ private String deploymentInfo = null;
+ private String validationOverrides = null;
+ private boolean failOnValidateXml = false;
+
+ public Builder() {
+ }
+
+ public Builder withEmptyHosts() {
+ return this.withHosts(emptyHosts);
+ }
+
+ public Builder withHosts(String hosts) {
+ this.hosts = hosts;
+ return this;
+ }
+
+ public Builder withEmptyServices() {
+ return this.withServices(emptyServices);
+ }
+
+ public Builder withServices(String services) {
+ this.services = services;
+ return this;
+ }
+
+ public Builder withSearchDefinition(String searchDefinition) {
+ this.searchDefinitions = Collections.singletonList(searchDefinition);
+ return this;
+ }
+
+ public Builder withSearchDefinitions(List<String> searchDefinition) {
+ this.searchDefinitions = Collections.unmodifiableList(searchDefinition);
+ return this;
+ }
+
+ public Builder withSearchDefinitionDir(String searchDefinitionDir) {
+ this.searchDefinitionDir = searchDefinitionDir;
+ return this;
+ }
+
+ public Builder withDeploymentInfo(String deploymentInfo) {
+ this.deploymentInfo = deploymentInfo;
+ return this;
+ }
+
+ public Builder withValidationOverrides(String validationOverrides) {
+ this.validationOverrides = validationOverrides;
+ return this;
+ }
+
+ public Builder failOnValidateXml() {
+ this.failOnValidateXml = true;
+ return this;
+ }
+
+ public ApplicationPackage build() {
+ return new MockApplicationPackage(hosts, services, searchDefinitions, searchDefinitionDir,
+ deploymentInfo, validationOverrides, failOnValidateXml);
+ }
+ }
+
+ public static String createSearchDefinition(String name, String fieldName) {
+ return "search " + name + " {" +
+ " document " + name + " {" +
+ " field " + fieldName + " type string {}" +
+ " }" +
+ "}";
+ }
+
+ private static final String emptyServices = "<services version=\"1.0\">" +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node1\" />" +
+ " </admin>" +
+ "</services>";
+
+ private static final String emptyHosts = "<hosts>" +
+ " <host name=\"localhost\">" +
+ " <alias>node1</alias>" +
+ " </host>" +
+ "</hosts>";
+
+ @Override
+ public void validateXML(DeployLogger logger) throws IOException {
+ if (failOnValidateXml) {
+ throw new IllegalArgumentException("Error in application package");
+ } else {
+ throw new UnsupportedOperationException("This application package cannot validate XML");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/test/MockRoot.java b/config-model/src/main/java/com/yahoo/config/model/test/MockRoot.java
new file mode 100644
index 00000000000..fa84cf1c7eb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/test/MockRoot.java
@@ -0,0 +1,169 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.test;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.ConfigProducer;
+import com.yahoo.vespa.model.HostSystem;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.builder.xml.dom.DomAdminV2Builder;
+import com.yahoo.vespa.model.filedistribution.FileDistributionConfigProducer;
+import com.yahoo.vespa.model.filedistribution.FileDistributor;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+
+
+/**
+ * Use for testing. Use as parent for the config producer(s) you want to test, to test
+ * only a subtree of the producers.
+ *
+ * @author gjoranv
+ */
+public class MockRoot extends AbstractConfigProducerRoot {
+ private static final long serialVersionUID = 1L;
+ public static final String MOCKHOST = "mockhost";
+
+ private HostSystem hostSystem;
+
+ private final DeployState deployState;
+ private FileDistributor fileDistributor;
+ private Admin admin;
+
+ public MockRoot() {
+ this("");
+ }
+
+ public MockRoot(String rootConfigId) {
+ this(rootConfigId, new MockApplicationPackage.Builder().build());
+ }
+
+ public MockRoot(String rootConfigId, ApplicationPackage applicationPackage) {
+ this(rootConfigId, new DeployState.Builder().applicationPackage(applicationPackage).build());
+ }
+
+ public MockRoot(String rootConfigId, DeployState deployState) {
+ super(rootConfigId);
+ hostSystem = new HostSystem(this, "hostsystem", deployState.getProvisioner());
+ this.deployState = deployState;
+ fileDistributor = new FileDistributor(deployState.getFileRegistry());
+ }
+
+ public FileDistributionConfigProducer getFileDistributionConfigProducer() {
+ return null;
+ }
+
+ @Override
+ public ConfigModelRepo configModelRepo() {
+ return new ConfigModelRepo();
+ }
+
+ public Set<String> getConfigIds() {
+ return Collections.unmodifiableSet(id2producer.keySet());
+ }
+
+ @Override
+ public ConfigInstance.Builder getConfig(ConfigInstance.Builder builder, String configId) {
+ ConfigProducer cp = id2producer.get(configId);
+ if (cp == null) return null;
+
+ cp.cascadeConfig(builder);
+ cp.addUserConfig(builder);
+ return builder;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T extends ConfigInstance> T getConfig(Class<T> configClass, String configId) {
+ try {
+ ConfigInstance.Builder builder = getConfig(getBuilder(configClass).newInstance(), configId);
+ return configClass.getConstructor(builder.getClass()).newInstance(builder);
+ } catch (InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ConfigProducer getProducer(String configId) {
+ return id2producer.get(configId);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T extends ConfigInstance> Class<? extends ConfigInstance.Builder> getBuilder(Class<T> configClass) {
+ for (Class<?> memberClass : configClass.getClasses()) {
+ if (memberClass.getSimpleName().equals("Builder"))
+ return (Class<? extends ConfigInstance.Builder>) memberClass;
+ }
+ throw new RuntimeException("Missing builder");
+ }
+
+ @Override
+ public DeployState getDeployState() {
+ return deployState;
+ }
+
+ public FileDistributor getFileDistributor() {
+ return fileDistributor;
+ }
+
+ public HostSystem getHostSystem() {
+ return hostSystem;
+ }
+
+ public void addDescendant(String configId, AbstractConfigProducer descendant) {
+ if (id2producer.containsKey(configId)) {
+ throw new RuntimeException
+ ("Config ID '" + configId + "' cannot be reserved by an instance of class '" +
+ descendant.getClass().getName() + "' since it is already used by an instance of class '" +
+ id2producer.get(configId).getClass().getName() + "'");
+ }
+ id2producer.put(configId, descendant);
+ }
+
+ @Override
+ public void addChild(AbstractConfigProducer abstractConfigProducer) {
+ super.addChild(abstractConfigProducer);
+ }
+
+ public final void setAdmin(String xml) {
+ String servicesXml =
+ "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services>" + xml + "</services>";
+
+ try {
+ Document doc = XmlHelper.getDocumentBuilder().parse(new InputSource(new StringReader(servicesXml)));
+ setAdmin(new DomAdminV2Builder(deployState.getFileRegistry(), false, new ArrayList<>()).
+ build(this, XML.getChildren(doc.getDocumentElement(), "admin").get(0)));
+ } catch (SAXException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public final void setAdmin(Admin admin) {
+ this.admin = admin;
+ }
+
+ @Override
+ public final Admin getAdmin() {
+ return admin;
+ }
+
+ @Override
+ public DeployLogger deployLogger() {
+ return new BaseDeployLogger();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/test/TestDriver.java b/config-model/src/main/java/com/yahoo/config/model/test/TestDriver.java
new file mode 100644
index 00000000000..4518e44d441
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/test/TestDriver.java
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.test;
+
+import com.google.common.annotations.Beta;
+import com.yahoo.config.model.MapConfigModelRegistry;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.SchemaValidator;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.vespa.model.VespaModel;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test driver for testing config models. Add custom builders for plugins to be tested. Builds a model from the
+ * xml string and returns a config producer that can be use to test getConfig.
+ *
+ * @author lulf
+ * @since 5.1.20
+ */
+@Beta
+public class TestDriver {
+
+ private final List<ConfigModelBuilder> builders = new ArrayList<>();
+ private final boolean validate;
+
+ public TestDriver(boolean validate) {
+ this.validate = validate;
+ }
+
+ public TestDriver() {
+ this(false);
+ }
+
+ /**
+ * Add a new builder to the tester.
+ *
+ * @param builder builder to add.
+ * @return this for chaining
+ */
+ public TestDriver addBuilder(ConfigModelBuilder builder) {
+ builders.add(builder);
+ return this;
+ }
+
+ /**
+ * Build a model from an XML string. The hosts referenced in services must be set to 'mockhost' when using
+ * this method, as it automatically generates a hosts file for you.
+ *
+ * @param servicesXml The xml for services.xml
+ * @return a producer root capable of answering getConfig requests.
+ */
+ public TestRoot buildModel(String servicesXml) {
+ return buildModel(servicesXml, "<hosts><host name='localhost'><alias>mockhost</alias></host></hosts>");
+ }
+
+ /**
+ * Build a model from an XML string of services and one of hosts.
+ *
+ * @param servicesXml The xml for services.xml
+ * @param hostsXml The xml for hosts.xml
+ * @return a producer root capable of answering getConfig requests.
+ */
+ public TestRoot buildModel(String servicesXml, String hostsXml) {
+ if (!servicesXml.contains("<services")) {
+ servicesXml = "<services version='1.0'>" + servicesXml + "</services>";
+ }
+ return buildModel(new MockApplicationPackage.Builder().withHosts(hostsXml).withServices(servicesXml).build());
+ }
+
+ /**
+ * Build a model from an application package.
+ *
+ * @param applicationPackage Any type of application package.
+ * @return a producer root capable of answering getConfig requests.
+ */
+ public TestRoot buildModel(ApplicationPackage applicationPackage) {
+ return buildModel(new DeployState.Builder().applicationPackage(applicationPackage).build());
+ }
+
+ /**
+ * Build a model given a deploy state.
+ *
+ * @param deployState An instance of {@link com.yahoo.config.model.deploy.DeployState}
+ * @return a producer root capable of answering getConfig requests.
+ */
+ public TestRoot buildModel(DeployState deployState) {
+ MapConfigModelRegistry registry = new MapConfigModelRegistry(builders);
+ try {
+ validate(deployState.getApplicationPackage());
+ return new TestRoot(new VespaModel(registry, deployState));
+ } catch (IOException | SAXException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void validate(ApplicationPackage appPkg) throws IOException {
+ if (!validate) {
+ return;
+ }
+ SchemaValidator validator = SchemaValidator.createTestValidatorHosts();
+ if (appPkg.getHosts() != null) {
+ validator.validate(appPkg.getHosts());
+ }
+ validator = SchemaValidator.createTestValidatorServices();
+ validator.validate(appPkg.getServices());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/test/TestRoot.java b/config-model/src/main/java/com/yahoo/config/model/test/TestRoot.java
new file mode 100644
index 00000000000..44491e33479
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/test/TestRoot.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.test;
+
+import com.google.common.annotations.Beta;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.VespaModel;
+
+import java.util.List;
+
+/**
+ * Test utility class that provides many methods for inspecting the state of a completely built model
+ *
+ * @author lulf
+ * @since 5.1
+ */
+@Beta
+public class TestRoot {
+ private final VespaModel model;
+ TestRoot(VespaModel model) {
+ this.model = model;
+ }
+
+ /**
+ * Get a list of all config models of a particular type.
+ *
+ * @param clazz The class of the models to find.
+ * @return A list of models of given type.
+ */
+ public <MODEL extends ConfigModel> List<MODEL> getConfigModels(Class<MODEL> clazz) {
+ return model.configModelRepo().getModels(clazz);
+ }
+
+ /**
+ * Ask model to populate builder with config for a given config id. This method gives the same config as the
+ * configserver would return to its clients.
+ * @param builder The builder to populate
+ * @param configId The config id of the producer to ask for config.
+ * @return the same builder.
+ */
+ public <BUILDER extends ConfigInstance.Builder> BUILDER getConfig(BUILDER builder, String configId) {
+ return (BUILDER) model.getConfig(builder, configId);
+ }
+
+ /**
+ * Request config of a given type and id. This method gives the same config as the configserver would return to
+ * its clients.
+ *
+ * @param clazz Type of config to request.
+ * @param configId The config id of the producer to ask for config.
+ * @return A config object of the appropriate type with config values set.
+ */
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> clazz, String configId) {
+ return model.getConfig(clazz, configId);
+ }
+
+ /**
+ * Retrieve the hosts available in this model. Useful to verify that hostnames are set correctly etc.
+ *
+ * @return A list of hosts.
+ */
+ public List<HostResource> getHosts() {
+ return model.getHostSystem().getHosts();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/config/model/test/package-info.java b/config-model/src/main/java/com/yahoo/config/model/test/package-info.java
new file mode 100644
index 00000000000..6c33505df83
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/config/model/test/package-info.java
@@ -0,0 +1,6 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+//TODO: Temporary export due to standalone container package, remove later.
+@ExportPackage
+package com.yahoo.config.model.test;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/documentmodel/DataTypeCollection.java b/config-model/src/main/java/com/yahoo/documentmodel/DataTypeCollection.java
new file mode 100644
index 00000000000..822ddcd0da7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/documentmodel/DataTypeCollection.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.documentmodel;
+
+import com.yahoo.document.DataType;
+
+import java.util.Collection;
+
+/**
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public interface DataTypeCollection {
+ public DataType getDataType(String name);
+ public DataType getDataType(int id);
+ public Collection<DataType> getTypes();
+}
diff --git a/config-model/src/main/java/com/yahoo/documentmodel/DataTypeRepo.java b/config-model/src/main/java/com/yahoo/documentmodel/DataTypeRepo.java
new file mode 100644
index 00000000000..6d332ba16fb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/documentmodel/DataTypeRepo.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.documentmodel;
+
+import com.yahoo.document.DataType;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class DataTypeRepo implements DataTypeCollection {
+ Map<Integer, DataType> typeById = new LinkedHashMap<>();
+ Map<String, DataType> typeByName = new LinkedHashMap<>();
+
+ public DataType getDataType(String name) {
+ return typeByName.get(name);
+ }
+
+ public DataType getDataType(int id) {
+ return typeById.get(id);
+ }
+
+ public Collection<DataType> getTypes() { return typeById.values(); }
+
+ public DataTypeRepo add(DataType type) {
+ if (typeByName.containsKey(type.getName()) ||
+ typeById.containsKey(type.getId()))
+ {
+ throw new IllegalStateException("Data type '" + type.getName() + "', id '" + type.getId() + "' is already registered.");
+ }
+ typeByName.put(type.getName(), type);
+ typeById.put(type.getId(), type);
+ return this;
+ }
+
+ public DataTypeRepo addAll(DataTypeCollection repo) {
+ for (DataType dataType : repo.getTypes()) {
+ add(dataType);
+ }
+ return this;
+ }
+
+ public DataTypeRepo replace(DataType type) {
+ if (!typeByName.containsKey(type.getName()) ||
+ !typeById.containsKey(type.getId()))
+ {
+ throw new IllegalStateException("Data type '" + type.getName() + "' is not registered.");
+ }
+ typeByName.remove(type.getName());
+ typeByName.put(type.getName(), type);
+ typeById.remove(type.getId());
+ typeById.put(type.getId(), type);
+ return this;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeCollection.java b/config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeCollection.java
new file mode 100644
index 00000000000..6c71410c048
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeCollection.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.documentmodel;
+
+import java.util.Collection;
+
+/**
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public interface DocumentTypeCollection {
+ public NewDocumentType getDocumentType(NewDocumentType.Name name);
+ public NewDocumentType getDocumentType(int id);
+ public Collection<NewDocumentType> getTypes();
+}
diff --git a/config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeRepo.java b/config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeRepo.java
new file mode 100644
index 00000000000..3585a12ac2f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/documentmodel/DocumentTypeRepo.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.documentmodel;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class DocumentTypeRepo implements DocumentTypeCollection {
+ final Map<Integer, NewDocumentType> typeById = new LinkedHashMap<>();
+ final Map<NewDocumentType.Name, NewDocumentType> typeByName = new LinkedHashMap<>();
+
+ public final NewDocumentType getDocumentType(String name) {
+ return typeByName.get(new NewDocumentType.Name(name));
+ }
+ public NewDocumentType getDocumentType(NewDocumentType.Name name) {
+ return typeByName.get(name);
+ }
+
+ public NewDocumentType getDocumentType(int id) {
+ return typeById.get(id);
+ }
+
+ public Collection<NewDocumentType> getTypes() { return typeById.values(); }
+
+ public DocumentTypeRepo add(NewDocumentType type) {
+ if (typeByName.containsKey(type.getFullName())) {
+ throw new IllegalStateException("Document type " + type.toString() + " is already registered");
+ }
+ if (typeById.containsKey(type.getFullName().getId())) {
+ throw new IllegalStateException("Document type " + type.toString() + " is already registered");
+ }
+ typeByName.put(type.getFullName(), type);
+ typeById.put(type.getFullName().getId(), type);
+ return this;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/documentmodel/NewDocumentType.java b/config-model/src/main/java/com/yahoo/documentmodel/NewDocumentType.java
new file mode 100644
index 00000000000..51171e97704
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/documentmodel/NewDocumentType.java
@@ -0,0 +1,366 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.documentmodel;
+
+import com.yahoo.document.*;
+import com.yahoo.document.annotation.AnnotationType;
+import com.yahoo.document.annotation.AnnotationTypeRegistry;
+import com.yahoo.document.datatypes.FieldValue;
+import com.yahoo.searchdefinition.FieldSets;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.FieldSet;
+import com.yahoo.searchdefinition.processing.BuiltInFieldSets;
+
+import java.util.*;
+
+/**
+ * TODO: What is this and why?
+ *
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public final class NewDocumentType extends StructuredDataType implements DataTypeCollection {
+
+ /**
+ * TODO: What is this and why?
+ */
+ public static final class Name {
+
+ // TODO: privatize
+ final String name;
+ final int id;
+
+ public Name(String name) {
+ this(name.hashCode(),name);
+ }
+
+ public Name(int id,String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public String toString() { return name; }
+
+ public final String getName() { return name; }
+
+ public final int getId() { return id; }
+
+ public int hashCode() { return name.hashCode(); }
+
+ public boolean equals(Object other) {
+ if ( ! (other instanceof Name)) return false;
+ return name.equals(((Name)other).getName());
+ }
+ }
+
+ private final Name name;
+ private final DataTypeRepo dataTypes = new DataTypeRepo();
+ private final Map<Integer, NewDocumentType> inherits = new LinkedHashMap<>();
+ private final AnnotationTypeRegistry annotations = new AnnotationTypeRegistry();
+ private final StructDataType header;
+ private final StructDataType body;
+ private final Set<FieldSet> fieldSets = new LinkedHashSet<>();
+
+ public NewDocumentType(Name name) {
+ this(name,
+ new StructDataType(name.getName() + ".header"),
+ new StructDataType(name.getName() + ".body"), new FieldSets());
+ }
+ public NewDocumentType(Name name, StructDataType header, StructDataType body, FieldSets fs) {
+ super(name.getName());
+ this.name = name;
+ this.header = header;
+ this.body = body;
+ if (fs != null) {
+ this.fieldSets.addAll(fs.userFieldSets().values());
+ for (FieldSet f : fs.builtInFieldSets().values()) {
+ if ((f.getName() != BuiltInFieldSets.INTERNAL_FIELDSET_NAME) &&
+ (f.getName() != BuiltInFieldSets.SEARCH_FIELDSET_NAME)) {
+ fieldSets.add(f);
+ }
+ }
+ }
+ }
+
+ public Name getFullName() {
+ return name;
+ }
+
+ public DataType getHeader() { return header; }
+ public DataType getBody() { return body; }
+ public Collection<NewDocumentType> getInherited() { return inherits.values(); }
+ public NewDocumentType getInherited(Name inherited) { return inherits.get(inherited.getId()); }
+ public NewDocumentType removeInherited(Name inherited) { return inherits.remove(inherited.getId()); }
+
+ /**
+ * Data type of the header fields of this and all inherited document types
+ * @return merged {@link StructDataType}
+ */
+ public StructDataType allHeader() {
+ StructDataType ret = new StructDataType(header.getName());
+ for (Field f : header.getFields()) {
+ ret.addField(f);
+ }
+ for (NewDocumentType inherited : getInherited()) {
+ for (Field f : ((StructDataType) inherited.getHeader()).getFields()) {
+ ret.addField(f);
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Data type of the body fields of this and all inherited document types
+ * @return merged {@link StructDataType}
+ */
+ public StructDataType allBody() {
+ StructDataType ret = new StructDataType(body.getName());
+ for (Field f : body.getFields()) {
+ ret.addField(f);
+ }
+ for (NewDocumentType inherited : getInherited()) {
+ for (Field f : ((StructDataType) inherited.getBody()).getFields()) {
+ ret.addField(f);
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public Class getValueClass() {
+ return Document.class;
+ }
+
+ @Override
+ public boolean isValueCompatible(FieldValue value) {
+ if (!(value instanceof Document)) {
+ return false;
+ }
+ /** Temporary disabled due to clash with document and covariant return type
+ Document doc = (Document) value;
+ if (((NewDocumentType) doc.getDataType()).inherits(this)) {
+ //the value is of this type; or the supertype of the value is of this type, etc....
+ return true;
+ }
+ */
+ return false;
+ }
+
+ private boolean verifyInheritance(NewDocumentType inherited) {
+ for (Field f : getFields()) {
+ Field inhF = inherited.getField(f.getName());
+ if (inhF != null && !inhF.equals(f)) {
+ throw new IllegalArgumentException("Inherited document '" + inherited.toString() + "' already contains field '" +
+ inhF.getName() + "'. Can not override with '" + f.getName() + "'.");
+ }
+ }
+ for (Field f : inherited.getAllFields()) {
+ for (NewDocumentType side : inherits.values()) {
+ Field sideF = side.getField(f.getName());
+ if (sideF != null && !sideF.equals(f)) {
+ throw new IllegalArgumentException("Inherited document '" + side.toString() + "' already contains field '" +
+ sideF.getName() + "'. Document '" + inherited.toString() + "' also defines field '" + f.getName() +
+ "'.Multiple inheritance must be disjunctive.");
+ }
+ }
+ }
+ return true;
+ }
+ public void inherit(NewDocumentType inherited) {
+ if ( ! inherits.containsKey(inherited.getId())) {
+ verifyInheritance(inherited);
+ inherits.put(inherited.getId(), inherited);
+ }
+ }
+ public boolean inherits(NewDocumentType superType) {
+ if (getId() == superType.getId()) return true;
+ for (NewDocumentType type : inherits.values()) {
+ if (type.inherits(superType)) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Field getField(String name) {
+ Field field = header.getField(name);
+ if (field == null) {
+ field = body.getField(name);
+ }
+ if (field == null) {
+ for (NewDocumentType inheritedType : inherits.values()) {
+ field = inheritedType.getField(name);
+ if (field != null) {
+ return field;
+ }
+ }
+ }
+ return field;
+ }
+
+ public boolean containsField(String fieldName) {
+ return getField(fieldName) != null;
+ }
+
+ @Override
+ public Field getField(int id) {
+ Field field = header.getField(id);
+ if (field == null) {
+ field = body.getField(id);
+ }
+ if (field == null) {
+ for (NewDocumentType inheritedType : inherits.values()) {
+ field = inheritedType.getField(id);
+ if (field != null) {
+ return field;
+ }
+ }
+ }
+ return field;
+ }
+
+ public Collection<Field> getAllFields() {
+ Collection<Field> collection = new LinkedList<>();
+
+ for (NewDocumentType type : inherits.values()) {
+ collection.addAll(type.getAllFields());
+ }
+
+ collection.addAll(header.getFields());
+ collection.addAll(body.getFields());
+ return Collections.unmodifiableCollection(collection);
+ }
+
+ public Collection<Field> getFields() {
+ Collection<Field> collection = new LinkedList<>();
+ collection.addAll(header.getFields());
+ collection.addAll(body.getFields());
+ return Collections.unmodifiableCollection(collection);
+ }
+
+ @Override
+ public Document createFieldValue() {
+ return new Document(null, (DocumentId)null);
+ }
+
+ @Override
+ public Collection<DataType> getTypes() {
+ return dataTypes.getTypes();
+ }
+
+ public DataTypeCollection getAllTypes() {
+ DataTypeRepo repo = new DataTypeRepo();
+ Set<Name> seen = new HashSet<>();
+ Deque<NewDocumentType> stack = new LinkedList<>();
+ stack.push(this);
+ while (!stack.isEmpty()) {
+ NewDocumentType docType = stack.pop();
+ if (seen.contains(docType.name)) {
+ continue; // base type
+ }
+ seen.add(docType.name);
+ for (DataType dataType : docType.getTypes()) {
+ if (repo.getDataType(dataType.getId()) == null) {
+ repo.add(dataType);
+ }
+ }
+ stack.addAll(docType.inherits.values());
+ }
+ return repo;
+ }
+
+ public Collection<AnnotationType> getAnnotations() { return annotations.getTypes().values(); }
+ public Collection<AnnotationType> getAllAnnotations() {
+ Collection<AnnotationType> collection = new LinkedList<>();
+
+ for (NewDocumentType type : inherits.values()) {
+ collection.addAll(type.getAllAnnotations());
+ }
+ collection.addAll(getAnnotations());
+
+ return Collections.unmodifiableCollection(collection);
+ }
+
+ public DataType getDataType(String name) {
+ return dataTypes.getDataType(name);
+ }
+ public DataType getDataType(int id) {
+ return dataTypes.getDataType(id);
+ }
+ public DataType getDataTypeRecursive(String name) {
+ DataType a = dataTypes.getDataType(name);
+ if (a != null) {
+ return a;
+ } else {
+ for (NewDocumentType dt : getInherited()) {
+ a = dt.getDataTypeRecursive(name);
+ if (a != null) {
+ return a;
+ }
+ }
+ }
+ return null;
+ }
+
+ public DataType getDataTypeRecursive(int id) {
+ DataType a = dataTypes.getDataType(id);
+ if (a != null) {
+ return a;
+ } else {
+ for (NewDocumentType dt : getInherited()) {
+ a = dt.getDataTypeRecursive(id);
+ if (a != null) {
+ return a;
+ }
+ }
+ }
+ return null;
+ }
+
+ public AnnotationType getAnnotationType(String name) {
+ AnnotationType a = annotations.getType(name);
+ if (a != null) {
+ return a;
+ } else {
+ for (NewDocumentType dt : getInherited()) {
+ a = dt.getAnnotationType(name);
+ if (a != null) {
+ return a;
+ }
+ }
+ }
+ return null;
+ }
+ public AnnotationType getAnnotationType(int id) {
+ AnnotationType a = annotations.getType(id);
+ if (a != null) {
+ return a;
+ } else {
+ for (NewDocumentType dt : getInherited()) {
+ a = dt.getAnnotationType(id);
+ if (a != null) {
+ return a;
+ }
+ }
+ }
+ return null;
+ }
+
+ public NewDocumentType add(AnnotationType type) {
+ annotations.register(type);
+ return this;
+ }
+ public NewDocumentType add(DataType type) {
+ dataTypes.add(type);
+ return this;
+ }
+ public NewDocumentType replace(DataType type) {
+ dataTypes.replace(type);
+ return this;
+ }
+
+ /**
+ * The field sets defined for this type and its {@link Search}
+ * @return fieldsets
+ */
+ public Set<FieldSet> getFieldSets() {
+ return Collections.unmodifiableSet(fieldSets);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/documentmodel/VespaDocumentType.java b/config-model/src/main/java/com/yahoo/documentmodel/VespaDocumentType.java
new file mode 100644
index 00000000000..bf8ec8d3da7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/documentmodel/VespaDocumentType.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.documentmodel;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.DataTypeName;
+import com.yahoo.document.PositionDataType;
+
+/**
+ * This class represents the builtin 'doument' document type that all other documenttypes inherits.
+ * Remember that changes here must be compatible. Changes to types of fields can not be done here.
+ * This must also match the mirroring class in c++.
+ *
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class VespaDocumentType {
+
+ public static NewDocumentType INSTANCE = newInstance();
+
+ public static DataTypeName NAME = new DataTypeName("document");
+
+ private static NewDocumentType newInstance() {
+ NewDocumentType vespa = new NewDocumentType(new NewDocumentType.Name(8, "document"));
+ vespa.add(DataType.BYTE);
+ vespa.add(DataType.INT);
+ vespa.add(DataType.LONG);
+ vespa.add(DataType.STRING);
+ vespa.add(DataType.RAW);
+ vespa.add(DataType.TAG);
+ vespa.add(DataType.FLOAT);
+ vespa.add(DataType.DOUBLE);
+ vespa.add(DataType.DOCUMENT);
+ vespa.add(PositionDataType.INSTANCE);
+ vespa.add(DataType.URI);
+ vespa.add(DataType.PREDICATE);
+ vespa.add(DataType.TENSOR);
+ return vespa;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/ConstantTensorTransformer.java b/config-model/src/main/java/com/yahoo/searchdefinition/ConstantTensorTransformer.java
new file mode 100644
index 00000000000..98eb0a4b77c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/ConstantTensorTransformer.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchlib.rankingexpression.evaluation.TensorValue;
+import com.yahoo.searchlib.rankingexpression.evaluation.Value;
+import com.yahoo.searchlib.rankingexpression.rule.CompositeNode;
+import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode;
+import com.yahoo.searchlib.rankingexpression.rule.NameNode;
+import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
+import com.yahoo.searchlib.rankingexpression.transform.ExpressionTransformer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Transforms named references to constant tensors with the rank feature 'constant'.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+class ConstantTensorTransformer extends ExpressionTransformer {
+
+ private final Map<String, Value> constants;
+ private final Map<String, String> rankPropertiesOutput;
+
+ public ConstantTensorTransformer(Map<String, Value> constants,
+ Map<String, String> rankPropertiesOutput) {
+ this.constants = constants;
+ this.rankPropertiesOutput = rankPropertiesOutput;
+ }
+
+ @Override
+ public ExpressionNode transform(ExpressionNode node) {
+ if (node instanceof ReferenceNode) {
+ return transformFeature((ReferenceNode) node);
+ } else if (node instanceof CompositeNode) {
+ return transformChildren((CompositeNode) node);
+ } else {
+ return node;
+ }
+ }
+
+ private ExpressionNode transformFeature(ReferenceNode node) {
+ if (!node.getArguments().isEmpty()) {
+ return transformArguments(node);
+ } else {
+ return transformConstantReference(node);
+ }
+ }
+
+ private ExpressionNode transformArguments(ReferenceNode node) {
+ List<ExpressionNode> arguments = node.getArguments().expressions();
+ List<ExpressionNode> transformedArguments = new ArrayList<>(arguments.size());
+ for (ExpressionNode argument : arguments) {
+ transformedArguments.add(transform(argument));
+ }
+ return node.setArguments(transformedArguments);
+ }
+
+ private ExpressionNode transformConstantReference(ReferenceNode node) {
+ Value value = constants.get(node.getName());
+ if (value == null || !(value instanceof TensorValue)) {
+ return node;
+ }
+ TensorValue tensorValue = (TensorValue)value;
+ String featureName = "constant(" + node.getName() + ")";
+ String tensorType = (tensorValue.getType().isPresent() ? tensorValue.getType().get().toString() : "tensor");
+ rankPropertiesOutput.put(featureName + ".value", tensorValue.toString());
+ rankPropertiesOutput.put(featureName + ".type", tensorType);
+ return new ReferenceNode("constant", Arrays.asList(new NameNode(node.getName())), null);
+ }
+
+} \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/DefaultRankProfile.java b/config-model/src/main/java/com/yahoo/searchdefinition/DefaultRankProfile.java
new file mode 100644
index 00000000000..c0dcf27c88d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/DefaultRankProfile.java
@@ -0,0 +1,140 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * The rank profile containing default settings. This is derived from the fields
+ * whenever this is accessed.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class DefaultRankProfile extends RankProfile {
+
+ /**
+ * Creates a new rank profile
+ *
+ * @param rankProfileRegistry The {@link com.yahoo.searchdefinition.RankProfileRegistry} to use for storing and looking up rank profiles.
+ */
+ public DefaultRankProfile(Search search, RankProfileRegistry rankProfileRegistry) {
+ super("default", search, rankProfileRegistry);
+ }
+
+ /**
+ * Does nothing, the default rank profile can not inherit anything
+ */
+ // TODO: Why not? If that's the case, then fail attempts at it
+ public void setInherited(String inheritedName) {
+ }
+
+ /**
+ * Returns null, the default rank profile can not inherit anything
+ */
+ public String getInheritedName() {
+ return null;
+ }
+
+ /**
+ * Returns the rank boost value of the given field
+ */
+ public RankSetting getRankSetting(String fieldOrIndex,RankSetting.Type type) {
+ RankSetting setting=super.getRankSetting(fieldOrIndex,type);
+ if (setting!=null) return setting;
+
+ SDField field=getSearch().getField(fieldOrIndex);
+ if (field!=null) {
+ setting=toRankSetting(field,type);
+ if (setting!=null)
+ return setting;
+ }
+
+ Index index=getSearch().getIndex(fieldOrIndex);
+ if (index!=null) {
+ setting=toRankSetting(index,type);
+ if (setting!=null)
+ return setting;
+ }
+
+ return null;
+ }
+
+ private RankSetting toRankSetting(SDField field,RankSetting.Type type) {
+ if (type.equals(RankSetting.Type.WEIGHT) && field.getWeight()>0 && field.getWeight()!=100)
+ return new RankSetting(field.getName(),type,field.getWeight());
+ if (type.equals(RankSetting.Type.RANKTYPE))
+ return new RankSetting(field.getName(),type,field.getRankType());
+ if (type.equals(RankSetting.Type.LITERALBOOST) && field.getLiteralBoost()>0)
+ return new RankSetting(field.getName(),type,field.getLiteralBoost());
+
+ // Index level setting really
+ if (type.equals(RankSetting.Type.PREFERBITVECTOR) && field.getRanking().isFilter()) {
+ return new RankSetting(field.getName(), type, true);
+ }
+
+ return null;
+ }
+
+ private RankSetting toRankSetting(Index index, RankSetting.Type type) {
+ /* TODO: Add support for indexes by adding a ranking object to the index
+ if (type.equals(RankSetting.Type.PREFERBITVECTOR) && index.isPreferBitVector()) {
+ return new RankSetting(index.getName(), type, new Boolean(true));
+ }
+ */
+ return null;
+ }
+
+ /**
+ * Returns the names of the fields which have a rank boost setting
+ * explicitly in this profile or in fields
+ */
+ public Set<RankSetting> rankSettings() {
+ Set<RankSetting> settings=new LinkedHashSet<>(20);
+ settings.addAll(this.rankSettings);
+ for (SDField field : getSearch().allFieldsList() ) {
+ addSetting(field,RankSetting.Type.WEIGHT,settings);
+ addSetting(field,RankSetting.Type.RANKTYPE,settings);
+ addSetting(field,RankSetting.Type.LITERALBOOST,settings);
+ addSetting(field,RankSetting.Type.PREFERBITVECTOR,settings);
+ }
+
+ // Foer settings that really pertains to indexes do the explicit indexes too
+ for (Index index : getSearch().getExplicitIndices()) {
+ addSetting(index,RankSetting.Type.PREFERBITVECTOR,settings);
+ }
+ return settings;
+ }
+
+ private void addSetting(SDField field,RankSetting.Type type,Set<RankSetting> settings) {
+ if (type.isIndexLevel()) {
+ addIndexSettings(field,type,settings);
+ }
+ else {
+ RankSetting setting=toRankSetting(field,type);
+ if (setting==null) return;
+ settings.add(setting);
+ }
+ }
+
+ private void addIndexSettings(SDField field,RankSetting.Type type,Set<RankSetting> settings) {
+ for (Iterator i = field.getFieldNameAsIterator(); i.hasNext(); ) {
+ String indexName=(String)i.next();
+ Index explicitIndex=field.getIndex(indexName);
+
+ // TODO: Make a ranking object in the index override the field level ranking object
+ if (type.equals(RankSetting.Type.PREFERBITVECTOR) && field.getRanking().isFilter()) {
+ settings.add(new RankSetting(indexName, type, true));
+ }
+ }
+ }
+
+ private void addSetting(Index index,RankSetting.Type type,Set<RankSetting> settings) {
+ RankSetting setting=toRankSetting(index,type);
+ if (setting==null) return;
+ settings.add(setting);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/DocumentModelBuilder.java b/config-model/src/main/java/com/yahoo/searchdefinition/DocumentModelBuilder.java
new file mode 100644
index 00000000000..4fd7048159e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/DocumentModelBuilder.java
@@ -0,0 +1,415 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.*;
+import com.yahoo.document.annotation.AnnotationReferenceDataType;
+import com.yahoo.document.annotation.AnnotationType;
+import com.yahoo.documentmodel.DataTypeCollection;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.documentmodel.VespaDocumentType;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.annotation.SDAnnotationType;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.annotation.TemporaryAnnotationReferenceDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import com.yahoo.vespa.documentmodel.FieldView;
+import com.yahoo.vespa.documentmodel.SearchDef;
+import com.yahoo.vespa.documentmodel.SearchField;
+
+import java.util.*;
+
+/**
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class DocumentModelBuilder {
+
+ public static class RetryLaterException extends IllegalArgumentException {
+ public RetryLaterException(String message) {
+ super(message);
+ }
+ }
+ private DocumentModel model;
+ private final Map<NewDocumentType, List<SDDocumentType>> scratchInheritsMap = new HashMap<>();
+ public DocumentModelBuilder(DocumentModel model) {
+ this.model = model;
+ model.getDocumentManager().add(VespaDocumentType.INSTANCE);
+ }
+ public boolean valid() {
+ return scratchInheritsMap.isEmpty();
+ }
+ public void addToModel(Collection<Search> searchList) {
+ List<SDDocumentType> docList = new LinkedList<>();
+ for (Search search : searchList) {
+ docList.add(search.getDocument());
+ }
+ docList = sortDocumentTypes(docList);
+ addDocumentTypes(docList);
+ for (Collection<Search> toAdd = tryAdd(searchList);
+ !toAdd.isEmpty() && (toAdd.size() < searchList.size()); toAdd = tryAdd(searchList)) {
+ searchList = toAdd;
+ }
+ }
+
+ private List<SDDocumentType> sortDocumentTypes(List<SDDocumentType> docList) {
+ Set<String> doneNames = new HashSet<>();
+ doneNames.add(SDDocumentType.VESPA_DOCUMENT.getName());
+ List<SDDocumentType> doneList = new LinkedList<>();
+ List<SDDocumentType> prevList = null;
+ List<SDDocumentType> nextList = docList;
+ while (prevList == null || nextList.size() < prevList.size()) {
+ prevList = nextList;
+ nextList = new LinkedList<>();
+ for (SDDocumentType doc : prevList) {
+ boolean isDone = true;
+ for (SDDocumentType inherited : doc.getInheritedTypes()) {
+ if (!doneNames.contains(inherited.getName())) {
+ isDone = false;
+ break;
+ }
+ }
+ if (isDone) {
+ doneNames.add(doc.getName());
+ doneList.add(doc);
+ } else {
+ nextList.add(doc);
+ }
+ }
+ }
+ if (!nextList.isEmpty()) {
+ throw new IllegalArgumentException("Could not resolve inheritance of document types " +
+ toString(prevList) + ".");
+ }
+ return doneList;
+ }
+
+ private static String toString(List<SDDocumentType> lst) {
+ StringBuilder out = new StringBuilder();
+ for (int i = 0, len = lst.size(); i < len; ++i) {
+ out.append("'").append(lst.get(i).getName()).append("'");
+ if (i < len - 2) {
+ out.append(", ");
+ } else if (i < len - 1) {
+ out.append(" and ");
+ }
+ }
+ return out.toString();
+ }
+
+ private Collection<Search> tryAdd(Collection<Search> searchList) {
+ Collection<Search> left = new ArrayList<>();
+ for (Search search : searchList) {
+ try {
+ addToModel(search);
+ } catch (RetryLaterException e) {
+ left.add(search);
+ }
+ }
+ return left;
+ }
+ public void addToModel(Search search) {
+ // Then we add the search specific stuff
+ SearchDef searchDef = new SearchDef(search.getName());
+ addSearchFields(search.extraFieldList(), searchDef);
+ for (Field f : search.getDocument().fieldSet()) {
+ addSearchField((SDField) f, searchDef);
+ }
+ for(SDField field : search.allFieldsList()) {
+ for(Attribute attribute : field.getAttributes().values()) {
+ if (!searchDef.getFields().containsKey(attribute.getName())) {
+ searchDef.add(new SearchField(new Field(attribute.getName(), field), !field.getIndices().isEmpty(), true));
+ }
+ }
+ }
+
+ for (Field f : search.getDocument().fieldSet()) {
+ addAlias((SDField) f, searchDef);
+ }
+ model.getSearchManager().add(searchDef);
+ }
+ private static void addSearchFields(Collection<SDField> fields, SearchDef searchDef) {
+ for (SDField field : fields) {
+ addSearchField(field, searchDef);
+ }
+ }
+ private static void addSearchField(SDField field, SearchDef searchDef) {
+
+ SearchField searchField =
+ new SearchField(field,
+ field.getIndices().containsKey(field.getName()) && field.getIndices().get(field.getName()).getType().equals(Index.Type.VESPA),
+ field.getAttributes().containsKey(field.getName()));
+ searchDef.add(searchField);
+
+ // Add field to views
+ addToView(field.getIndices().keySet(), searchField, searchDef);
+ }
+
+ private static void addAlias(SDField field, SearchDef searchDef) {
+ for (Map.Entry<String, String> entry : field.getAliasToName().entrySet()) {
+ searchDef.addAlias(entry.getKey(), entry.getValue());
+ }
+ }
+
+ private static void addToView(Collection<String> views, Field field, SearchDef searchDef) {
+ for (String viewName : views) {
+ addToView(viewName, field, searchDef);
+ }
+ }
+
+ private static void addToView(String viewName, Field field, SearchDef searchDef) {
+ if (searchDef.getViews().containsKey(viewName)) {
+ searchDef.getViews().get(viewName).add(field);
+ } else {
+ if (!searchDef.getFields().containsKey(viewName)) {
+ FieldView view = new FieldView(viewName);
+ view.add(field);
+ searchDef.add(view);
+ }
+ }
+ }
+ private void addDocumentTypes(List<SDDocumentType> docList) {
+ LinkedList<NewDocumentType> lst = new LinkedList<>();
+ for (SDDocumentType doc : docList) {
+ lst.add(convert(doc));
+ model.getDocumentManager().add(lst.getLast());
+ }
+ for(NewDocumentType doc : lst) {
+ resolveTemporaries(doc.getAllTypes(), lst);
+ }
+ }
+ private static void resolveTemporaries(DataTypeCollection dtc, Collection<NewDocumentType> docs) {
+ for (DataType type : dtc.getTypes()) {
+ resolveTemporariesRecurse(type, dtc, docs);
+ }
+ }
+ private static DataType resolveTemporariesRecurse(DataType type, DataTypeCollection repo,
+ Collection<NewDocumentType> docs) {
+ if (type instanceof TemporaryStructuredDataType) {
+ NewDocumentType docType = getDocumentType(docs, type.getId());
+ if (docType != null) {
+ type = docType;
+ return type;
+ }
+ DataType real = repo.getDataType(type.getId());
+ if (real == null) {
+ throw new NullPointerException("Can not find type '" + type.toString() + "', impossible.");
+ }
+ type = real;
+ } else if (type instanceof StructDataType) {
+ StructDataType dt = (StructDataType) type;
+ for (com.yahoo.document.Field field : dt.getFields()) {
+ if (field.getDataType() != type) {
+ field.setDataType(resolveTemporariesRecurse(field.getDataType(), repo, docs));
+ }
+ }
+ } else if (type instanceof MapDataType) {
+ MapDataType t = (MapDataType) type;
+ t.setKeyType(resolveTemporariesRecurse(t.getKeyType(), repo, docs));
+ t.setValueType(resolveTemporariesRecurse(t.getValueType(), repo, docs));
+ } else if (type instanceof CollectionDataType) {
+ CollectionDataType t = (CollectionDataType) type;
+ t.setNestedType(resolveTemporariesRecurse(t.getNestedType(), repo, docs));
+ }
+ return type;
+ }
+
+ private static NewDocumentType getDocumentType(Collection<NewDocumentType> docs, int id) {
+ for (NewDocumentType doc : docs) {
+ if (doc.getId() == id) {
+ return doc;
+ }
+ }
+ return null;
+ }
+
+ private static void specialHandleAnnotationReference(NewDocumentType docType, Field field) {
+ DataType fieldType = specialHandleAnnotationReferenceRecurse(docType, field.getName(), field.getDataType());
+ if (fieldType == null) {
+ return;
+ }
+ field.setDataType(fieldType);
+ }
+
+ private static DataType specialHandleAnnotationReferenceRecurse(NewDocumentType docType, String fieldName,
+ DataType dataType)
+ {
+ if (dataType instanceof TemporaryAnnotationReferenceDataType) {
+ TemporaryAnnotationReferenceDataType refType = (TemporaryAnnotationReferenceDataType)dataType;
+ if (refType.getId() != 0) {
+ return null;
+ }
+ AnnotationType target = docType.getAnnotationType(refType.getTarget());
+ if (target == null) {
+ throw new RetryLaterException("Annotation '" + refType.getTarget() + "' in reference '" + fieldName +
+ "' does not exist.");
+ }
+ dataType = new AnnotationReferenceDataType(target);
+ addType(docType, dataType);
+ return dataType;
+ } else if (dataType instanceof MapDataType) {
+ MapDataType mapType = (MapDataType)dataType;
+ DataType valueType = specialHandleAnnotationReferenceRecurse(docType, fieldName, mapType.getValueType());
+ if (valueType == null) {
+ return null;
+ }
+ mapType = mapType.clone();
+ mapType.setValueType(valueType);
+ addType(docType, mapType);
+ return mapType;
+ } else if (dataType instanceof CollectionDataType) {
+ CollectionDataType lstType = (CollectionDataType)dataType;
+ DataType nestedType = specialHandleAnnotationReferenceRecurse(docType, fieldName, lstType.getNestedType());
+ if (nestedType == null) {
+ return null;
+ }
+ lstType = lstType.clone();
+ lstType.setNestedType(nestedType);
+ addType(docType, lstType);
+ return lstType;
+ }
+ return null;
+ }
+
+ private static StructDataType handleStruct(NewDocumentType dt, SDDocumentType type) {
+ StructDataType s = new StructDataType(type.getName());
+ for (Field f : type.getDocumentType().getHeaderType().getFieldsThisTypeOnly()) {
+ specialHandleAnnotationReference(dt, f);
+ s.addField(f);
+ }
+ for (StructDataType inherited : type.getDocumentType().getHeaderType().getInheritedTypes()) {
+ s.inherit(inherited);
+ }
+ extractNestedTypes(dt, s);
+ addType(dt, s);
+ return s;
+ }
+
+ private static StructDataType handleStruct(NewDocumentType dt, StructDataType s) {
+ for (Field f : s.getFieldsThisTypeOnly()) {
+ specialHandleAnnotationReference(dt, f);
+ }
+ extractNestedTypes(dt, s);
+ addType(dt, s);
+ return s;
+ }
+ private static boolean anyParentsHavePayLoad(SDAnnotationType sa, SDDocumentType sdoc) {
+ if (sa.getInherits() != null) {
+ AnnotationType tmp = sdoc.findAnnotation(sa.getInherits());
+ SDAnnotationType inherited = (SDAnnotationType) tmp;
+ return ((inherited.getSdDocType() != null) || anyParentsHavePayLoad(inherited, sdoc));
+ }
+ return false;
+ }
+ private NewDocumentType convert(SDDocumentType sdoc) {
+ Map<AnnotationType, String> annotationInheritance = new HashMap<>();
+ Map<StructDataType, String> structInheritance = new HashMap<>();
+ NewDocumentType dt = new NewDocumentType(new NewDocumentType.Name(sdoc.getName()),
+ sdoc.getDocumentType().getHeaderType(),
+ sdoc.getDocumentType().getBodyType(),
+ sdoc.getFieldSets());
+ for (SDDocumentType n : sdoc.getInheritedTypes()) {
+ NewDocumentType.Name name = new NewDocumentType.Name(n.getName());
+ NewDocumentType inherited = model.getDocumentManager().getDocumentType(name);
+ if (inherited != null) {
+ dt.inherit(inherited);
+ }
+ }
+ for (SDDocumentType type : sdoc.getTypes()) {
+ if (type.isStruct()) {
+ handleStruct(dt, type);
+ } else {
+ throw new IllegalArgumentException("Data type '" + sdoc.getName() + "' is not a struct => tostring='" + sdoc.toString() + "'.");
+ }
+ }
+ for (AnnotationType annotation : sdoc.getAnnotations()) {
+ dt.add(annotation);
+ }
+ for (AnnotationType annotation : sdoc.getAnnotations()) {
+ SDAnnotationType sa = (SDAnnotationType) annotation;
+ if (annotation.getInheritedTypes().isEmpty() && (sa.getInherits() != null) ) {
+ annotationInheritance.put(annotation, sa.getInherits());
+ }
+ if (annotation.getDataType() == null) {
+ if (sa.getSdDocType() != null) {
+ StructDataType s = handleStruct(dt, sa.getSdDocType());
+ annotation.setDataType(s);
+ if ((sa.getInherits() != null)) {
+ structInheritance.put(s, "annotation."+sa.getInherits());
+ }
+ } else if (sa.getInherits() != null) {
+ StructDataType s = new StructDataType("annotation."+annotation.getName());
+ if (anyParentsHavePayLoad(sa, sdoc)) {
+ annotation.setDataType(s);
+ addType(dt, s);
+ }
+ structInheritance.put(s, "annotation."+sa.getInherits());
+ }
+ }
+ }
+ for (Map.Entry<AnnotationType, String> e : annotationInheritance.entrySet()) {
+ e.getKey().inherit(dt.getAnnotationType(e.getValue()));
+ }
+ for (Map.Entry<StructDataType, String> e : structInheritance.entrySet()) {
+ StructDataType s = (StructDataType)dt.getDataType(e.getValue());
+ if (s != null) {
+ e.getKey().inherit(s);
+ }
+ }
+ handleStruct(dt, sdoc.getDocumentType().getHeaderType());
+ handleStruct(dt, sdoc.getDocumentType().getBodyType());
+
+ extractDataTypesFromFields(dt, sdoc.fieldSet());
+ return dt;
+ }
+ private static void extractDataTypesFromFields(NewDocumentType dt, Collection<Field> fields) {
+ for (Field f : fields) {
+ DataType type = f.getDataType();
+ if (testAddType(dt, type)) {
+ extractNestedTypes(dt, type);
+ addType(dt, type);
+ }
+ }
+ }
+ private static void extractNestedTypes(NewDocumentType dt, DataType type) {
+ if (type instanceof StructDataType) {
+ StructDataType tmp = (StructDataType) type;
+ extractDataTypesFromFields(dt, tmp.getFieldsThisTypeOnly());
+ } else if (type instanceof DocumentType) {
+ throw new IllegalArgumentException("Can not handle nested document definitions. In document type '" + dt.getName().toString() +
+ "', we can not define document type '" + type.toString());
+ } else if (type instanceof CollectionDataType) {
+ CollectionDataType tmp = (CollectionDataType) type;
+ extractNestedTypes(dt, tmp.getNestedType());
+ addType(dt, tmp.getNestedType());
+ } else if (type instanceof MapDataType) {
+ MapDataType tmp = (MapDataType) type;
+ extractNestedTypes(dt, tmp.getKeyType());
+ extractNestedTypes(dt, tmp.getValueType());
+ addType(dt, tmp.getKeyType());
+ addType(dt, tmp.getValueType());
+ } else if (type instanceof TemporaryAnnotationReferenceDataType) {
+ throw new IllegalArgumentException(type.toString());
+ }
+ }
+ private static boolean testAddType(NewDocumentType dt, DataType type) { return internalAddType(dt, type, true); }
+ private static boolean addType(NewDocumentType dt, DataType type) { return internalAddType(dt, type, false); }
+ private static boolean internalAddType(NewDocumentType dt, DataType type, boolean dryRun) {
+ DataType oldType = dt.getDataTypeRecursive(type.getId());
+ if (oldType == null) {
+ if ( ! dryRun) {
+ dt.add(type);
+ }
+ return true;
+ } else if ((type instanceof StructDataType) && (oldType instanceof StructDataType)) {
+ StructDataType s = (StructDataType) type;
+ StructDataType os = (StructDataType) oldType;
+ if ((os.getFieldCount() == 0) && (s.getFieldCount() > os.getFieldCount())) {
+ if ( ! dryRun) {
+ dt.replace(type);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplier.java b/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplier.java
new file mode 100644
index 00000000000..9cb3dea36a8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplier.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class FieldOperationApplier {
+ public void process(SDDocumentType sdoc) {
+ if (!sdoc.isStruct()) {
+ apply(sdoc);
+ }
+ }
+
+ protected void apply(SDDocumentType type) {
+ for (Field field : type.fieldSet()) {
+ apply(field);
+ }
+ }
+
+ protected void apply(Field field) {
+ if (field instanceof SDField) {
+ SDField sdField = (SDField) field;
+ sdField.applyOperations();
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForSearch.java b/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForSearch.java
new file mode 100644
index 00000000000..301d78bbaec
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForSearch.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class FieldOperationApplierForSearch extends FieldOperationApplier {
+ @Override
+ public void process(SDDocumentType sdoc) {
+ //Do nothing
+ }
+
+ public void process(Search search) {
+ for (Field field : search.extraFieldList()) {
+ apply(field);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForStructs.java b/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForStructs.java
new file mode 100644
index 00000000000..5bef71ac920
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/FieldOperationApplierForStructs.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class FieldOperationApplierForStructs extends FieldOperationApplier {
+ @Override
+ public void process(SDDocumentType sdoc) {
+ for (SDDocumentType type : sdoc.getAllTypes()) {
+ if (type.isStruct()) {
+ apply(type);
+ copyFields(type, sdoc);
+ }
+ }
+ }
+
+ private void copyFields(SDDocumentType structType, SDDocumentType sdoc) {
+ //find all fields in OTHER types that have this type:
+ List<SDDocumentType> list = new ArrayList<>();
+ list.add(sdoc);
+ list.addAll(sdoc.getTypes());
+ for (SDDocumentType anyType : list) {
+ Iterator<Field> fields = anyType.fieldIterator();
+ while (fields.hasNext()) {
+ SDField field = (SDField) fields.next();
+ DataType structUsedByField = field.getFirstStructRecursive();
+ if (structUsedByField == null) {
+ continue;
+ }
+ if (structUsedByField.getName().equals(structType.getName())) {
+ //this field is using this type!!
+ field.populateWithStructFields(sdoc, field.getName(), field.getDataType(), field.isHeader(), 0);
+ field.populateWithStructMatching(sdoc, field.getName(), field.getDataType(), field.getMatching());
+ }
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/FieldSets.java b/config-model/src/main/java/com/yahoo/searchdefinition/FieldSets.java
new file mode 100644
index 00000000000..503977a8f49
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/FieldSets.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.yahoo.searchdefinition.document.FieldSet;
+
+/**
+ * The field sets owned by a {@link Search}
+ * Both built in and user defined.
+ * @author vegardh
+ *
+ */
+public class FieldSets {
+
+ private final Map<String, FieldSet> userFieldSets = new LinkedHashMap<>();
+ private final Map<String, FieldSet> builtInFieldSets = new LinkedHashMap<>();
+
+ /**
+ * Adds an entry to user field sets, creating entries as needed
+ *
+ * @param setName name of a field set
+ * @param field field to add to field set
+ */
+ public void addUserFieldSetItem(String setName, String field) {
+ if (userFieldSets.get(setName) == null) {
+ // First entry in this set
+ userFieldSets.put(setName, new FieldSet(setName));
+ }
+ userFieldSets.get(setName).addFieldName(field);
+ }
+
+ /**
+ * Adds an entry to built in field sets, creating entries as needed
+ *
+ * @param setName name of a field set
+ * @param field field to add to field set
+ */
+ public void addBuiltInFieldSetItem(String setName, String field) {
+ if (builtInFieldSets.get(setName) == null) {
+ // First entry in this set
+ builtInFieldSets.put(setName, new FieldSet(setName));
+ }
+ builtInFieldSets.get(setName).addFieldName(field);
+ }
+
+ /**
+ * The built in field sets, unmodifiable
+ * @return built in field sets
+ */
+ public Map<String, FieldSet> builtInFieldSets() {
+ return Collections.unmodifiableMap(builtInFieldSets);
+ }
+
+ /**
+ * The user defined field sets, unmodifiable
+ * @return user field sets
+ */
+ public Map<String, FieldSet> userFieldSets() {
+ return Collections.unmodifiableMap(userFieldSets);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/Index.java b/config-model/src/main/java/com/yahoo/searchdefinition/Index.java
new file mode 100644
index 00000000000..9ed7d8677b9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/Index.java
@@ -0,0 +1,203 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.document.BooleanIndexDefinition;
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.Stemming;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * An index definition in a search definition.
+ * Two indices are equal if they have the same name and the same settings, except
+ * alias settings (which are excluded).
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class Index implements Cloneable, Serializable {
+
+ public static enum Type {
+ VESPA("vespa");
+ private String name;
+ private Type(String name) { this.name = name; }
+ public String getName() { return name; }
+
+ }
+
+ // Please see hashCode, equals and copy when adding attributes to this
+
+ /** The search definition-unique name of this index */
+ private String name;
+
+ /** The rank type of this index */
+ private RankType rankType=null;
+
+ /** Whether this index supports prefix search */
+ private boolean prefix;
+
+ /** The list of aliases (Strings) to this index name */
+ private Set<String> aliases=new java.util.LinkedHashSet<>(1);
+
+ /**
+ * The stemming setting of this field, or null to use the default.
+ * Default is determined by the owning search definition.
+ */
+ private Stemming stemming=null;
+
+ /** Whether the content of this index is normalized */
+ private boolean normalized=true;
+
+ /** The set of all searchable fields which should be searched with this index. May not null */
+ public Set<String> matchGroup=new LinkedHashSet<>();
+
+ private Type type = Type.VESPA;
+
+ /** The boolean index definition, if set */
+ private BooleanIndexDefinition boolIndex;
+
+ public Index(String name) {
+ this(name, false);
+ }
+
+ public Index(String name, boolean prefix) {
+ this.name=name;
+ this.prefix=prefix;
+ }
+
+ public void setName(String name) { this.name=name; }
+
+ public String getName() { return name; }
+
+ /** Sets the rank type of this field */
+ public void setRankType(RankType rankType) { this.rankType=rankType; }
+
+ /** Returns the rank type of this field, or null if nothing is set */
+ public RankType getRankType() { return rankType; }
+
+ /** Return the stemming setting of this index, may be null */
+ public Stemming getStemming() { return stemming; }
+
+ /**
+ * Returns the (unmodifiable) set of searchable fields which should be searched
+ * when this index is searched. This is useful to specify that some attributes should be
+ * searched as well when an index is searched.
+ * This set is either empty, or if set contains both the name of this index, and some other
+ * indexes.
+ */
+ public Set<String> getMatchGroup() { return Collections.unmodifiableSet(matchGroup); }
+
+ /** Adds a searchable field name to be searched when this index is searched */
+ public void addToMatchGroup(String name) {
+ if (name.equals(this.name)) return;
+ if (matchGroup.size()==0) matchGroup.add(this.name);
+ matchGroup.add(name);
+ }
+
+ /**
+ * Whether this field should be stemmed in this search definition,
+ * this is never null
+ */
+ public Stemming getStemming(Search search) {
+ if (stemming!=null)
+ return stemming;
+ else
+ return search.getStemming();
+ }
+
+ /**
+ * Sets how this field should be stemmed, or set to null to use the default.
+ */
+ public void setStemming(Stemming stemming) { this.stemming=stemming; }
+
+ /** Returns whether this index supports prefix search, default is false */
+ public boolean isPrefix() { return prefix; }
+
+ /** Sets whether this index supports prefix search */
+ public void setPrefix(boolean prefix) { this.prefix=prefix; }
+
+ /** Adds an alias to this index name */
+ public void addAlias(String alias) {
+ aliases.add(alias);
+ }
+
+ /** Returns a read-only iterator of the aliases (Strings) to this index name */
+ public Iterator<String> aliasIterator() {
+ return Collections.unmodifiableSet(aliases).iterator();
+ }
+
+ public int hashCode() {
+ return name.hashCode() + ( prefix ? 17 : 0 );
+ }
+
+ public boolean equals(Object object) {
+ if ( ! (object instanceof Index)) return false;
+
+ Index other=(Index)object;
+ return
+ this.name.equals(other.name) &&
+ this.prefix==other.prefix &&
+ this.stemming==other.stemming &&
+ this.normalized==other.normalized;
+ }
+
+ public String toString() {
+ String rankTypeName=rankType==null ? "(none)" : rankType.name();
+ return
+ "index '" + name +
+ "' [ranktype: " + rankTypeName +
+ ", prefix: " + prefix + "]";
+ }
+
+ /** Makes a deep copy of this index */
+ public Object clone() {
+ try {
+ Index copy=(Index)super.clone();
+ copy.aliases=new LinkedHashSet<>(this.aliases);
+ return copy;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Programming error",e);
+ }
+ }
+
+ public Index copy() {
+ return (Index)clone();
+ }
+
+ /**
+ * The index engine type
+ * @return the type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Sets the index engine type
+ * @param type a index engine type
+ */
+ public void setType(Type type) {
+ this.type = type;
+ }
+
+ /**
+ * The boolean index definition
+ * @return the boolean index definition
+ */
+ public BooleanIndexDefinition getBooleanIndexDefiniton() {
+ return boolIndex;
+ }
+
+ /**
+ * Sets the boolean index definition
+ * @param def boolean index definition
+ */
+ public void setBooleanIndexDefiniton(BooleanIndexDefinition def) {
+ boolIndex = def;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/MacroInliner.java b/config-model/src/main/java/com/yahoo/searchdefinition/MacroInliner.java
new file mode 100644
index 00000000000..a248ca30a8e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/MacroInliner.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchlib.rankingexpression.rule.CompositeNode;
+import com.yahoo.searchlib.rankingexpression.rule.ExpressionNode;
+import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
+import com.yahoo.searchlib.rankingexpression.transform.ExpressionTransformer;
+
+import java.util.Map;
+
+/**
+ * Inlines macros in ranking expressions
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+class MacroInliner extends ExpressionTransformer {
+
+ private final Map<String, RankProfile.Macro> macros;
+
+ public MacroInliner(Map<String, RankProfile.Macro> macros) {
+ this.macros = macros;
+ }
+
+ @Override
+ public ExpressionNode transform(ExpressionNode node) {
+ if (node instanceof ReferenceNode)
+ return transformFeatureNode((ReferenceNode)node);
+ if (node instanceof CompositeNode)
+ return transformChildren((CompositeNode)node);
+ return node;
+ }
+
+ private ExpressionNode transformFeatureNode(ReferenceNode feature) {
+ RankProfile.Macro macro = macros.get(feature.getName());
+ if (macro == null) return feature;
+ return transform(macro.getRankingExpression().getRoot()); // inline recursively and return
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java b/config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java
new file mode 100644
index 00000000000..3e943377611
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/RankProfile.java
@@ -0,0 +1,968 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.search.query.ranking.Diversity;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.searchlib.rankingexpression.ExpressionFunction;
+import com.yahoo.searchlib.rankingexpression.FeatureList;
+import com.yahoo.searchlib.rankingexpression.RankingExpression;
+import com.yahoo.searchlib.rankingexpression.evaluation.TensorValue;
+import com.yahoo.searchlib.rankingexpression.evaluation.Value;
+import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
+import com.yahoo.searchlib.rankingexpression.transform.ConstantDereferencer;
+import com.yahoo.searchlib.rankingexpression.transform.Simplifier;
+import com.yahoo.config.application.api.ApplicationPackage;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Represents a rank profile - a named set of ranking settings
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RankProfile implements Serializable, Cloneable {
+
+ /** The search definition-unique name of this rank profile */
+ private final String name;
+
+ /** The search definition owning this profile, or null if none */
+ private Search search=null;
+
+ /** The name of the rank profile inherited by this */
+ private String inheritedName = null;
+
+ /** The match settings of this profile */
+ protected MatchPhaseSettings matchPhaseSettings = null;
+
+ /** The rank settings of this profile */
+ protected Set<RankSetting> rankSettings = new java.util.LinkedHashSet<>();
+
+ /** The ranking expression to be used for first phase */
+ private RankingExpression firstPhaseRanking= null;
+
+ /** The ranking expression to be used for second phase */
+ private RankingExpression secondPhaseRanking = null;
+
+ /** Number of hits to be reranked in second phase, -1 means use default */
+ private int rerankCount = -1;
+
+ /** Mysterious attribute */
+ private int keepRankCount = -1;
+
+ private int numThreadsPerSearch = -1;
+ private int numSearchPartitions = -1;
+
+ private double termwiseLimit = 1.0;
+
+ /** The drop limit used to drop hits with rank score less than or equal to this value */
+ private double rankScoreDropLimit = -Double.MAX_VALUE;
+
+ private Set<ReferenceNode> summaryFeatures;
+
+ private Set<ReferenceNode> rankFeatures;
+
+ /** The properties of this - a multimap */
+ private Map<String, List<RankProperty>> rankProperties = new LinkedHashMap<>();
+
+ private Boolean ignoreDefaultRankFeatures=null;
+
+ private String secondPhaseRankingString=null;
+
+ private String firstPhaseRankingString=null;
+
+ private Map<String, Macro> macros= new LinkedHashMap<>();
+
+ private Set<String> filterFields = new HashSet<>();
+
+ private final RankProfileRegistry rankProfileRegistry;
+
+ /** Constants in ranking expressions */
+ private Map<String, Value> constants = new HashMap<>();
+
+ private final TypeSettings attributeTypes = new TypeSettings();
+
+ private final TypeSettings queryFeatureTypes = new TypeSettings();
+
+ /**
+ * Creates a new rank profile
+ *
+ * @param name the name of the new profile
+ * @param search the search definition owning this profile
+ * @param rankProfileRegistry The {@link com.yahoo.searchdefinition.RankProfileRegistry} to use for storing and looking up rank profiles.
+ */
+ public RankProfile(String name, Search search, RankProfileRegistry rankProfileRegistry) {
+ this.name = name;
+ this.search = search;
+ this.rankProfileRegistry = rankProfileRegistry;
+ }
+
+ public String getName() { return name; }
+
+ /**
+ * Returns the search definition owning this, or null if none
+ *
+ * @return The search definition.
+ */
+ public Search getSearch() {
+ return search;
+ }
+
+ /**
+ * Sets the name of the rank profile this inherits. Both rank profiles must be present in the same search
+ * definition
+ *
+ * @param inheritedName The name of the profile that this inherits from.
+ */
+ public void setInherited(String inheritedName) {
+ this.inheritedName = inheritedName;
+ }
+
+ /**
+ * Returns the name of the profile this one inherits, or null if none is inherited
+ *
+ * @return The inherited name.
+ */
+ public String getInheritedName() {
+ return inheritedName;
+ }
+
+ /**
+ * Returns the inherited rank profile, or null if there is none
+ *
+ * @return The inherited profile.
+ */
+ public RankProfile getInherited() {
+ if (getSearch()==null) return getInheritedFromRegistry(inheritedName);
+ RankProfile inheritedInThisSearch = rankProfileRegistry.getRankProfile(search, inheritedName);
+ if (inheritedInThisSearch!=null) return inheritedInThisSearch;
+ return getInheritedFromRegistry(inheritedName);
+ }
+
+ private RankProfile getInheritedFromRegistry(String inheritedName) {
+ for (RankProfile r : rankProfileRegistry.allRankProfiles()) {
+ if (r.getName().equals(inheritedName)) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether this profile inherits (directly or indirectly) the given profile
+ *
+ * @param name The profile name to compare this to.
+ * @return Whether or not this inherits from the named profile.
+ */
+ public boolean inherits(String name) {
+ RankProfile parent = getInherited();
+ while (parent != null) {
+ if (parent.getName().equals(name))
+ return true;
+ parent = parent.getInherited();
+ }
+ return false;
+ }
+
+ /**
+ * change match settings
+ * @param settings The new match settings
+ **/
+ public void setMatchPhaseSettings(MatchPhaseSettings settings) {
+ settings.checkValid();
+ this.matchPhaseSettings = settings;
+ }
+
+ public MatchPhaseSettings getMatchPhaseSettings() {
+ MatchPhaseSettings settings = this.matchPhaseSettings;
+ if (settings != null ) return settings;
+ if (getInherited() != null) return getInherited().getMatchPhaseSettings();
+ return null;
+ }
+
+ public void addRankSetting(RankSetting rankSetting) {
+ rankSettings.add(rankSetting);
+ }
+
+ public void addRankSetting(String fieldName, RankSetting.Type type, Object value) {
+ addRankSetting(new RankSetting(fieldName, type, value));
+ }
+
+ /**
+ * Returns the a rank setting of a field, or null if there is no such rank setting in this profile
+ *
+ * @param field The field whose settings to return.
+ * @param type The type that the field is required to be.
+ * @return The rank setting found, or null.
+ */
+ public RankSetting getDeclaredRankSetting(String field, RankSetting.Type type) {
+ for (Iterator<RankSetting> i = declaredRankSettingIterator(); i.hasNext();) {
+ RankSetting setting = i.next();
+ if (setting.getFieldName().equals(field) &&
+ setting.getType().equals(type)) {
+ return setting;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a rank setting of field or index, or null if there is no such rank setting in this profile or one it
+ * inherits
+ *
+ * @param field The field whose settings to return.
+ * @param type The type that the field is required to be.
+ * @return The rank setting found, or null.
+ */
+ public RankSetting getRankSetting(String field, RankSetting.Type type) {
+ RankSetting rankSetting = getDeclaredRankSetting(field, type);
+ if (rankSetting != null) return rankSetting;
+
+ if (getInherited() != null) return getInherited().getRankSetting(field, type);
+
+ return null;
+ }
+
+ /**
+ * Returns the rank settings in this rank profile
+ *
+ * @return An iterator for the declared rank setting.
+ */
+ public Iterator<RankSetting> declaredRankSettingIterator() {
+ return Collections.unmodifiableSet(rankSettings).iterator();
+ }
+
+ /**
+ * Returns all settings in this profile or any profile it inherits
+ *
+ * @return An iterator for all rank settings of this.
+ */
+ public Iterator<RankSetting> rankSettingIterator() {
+ return rankSettings().iterator();
+ }
+
+ /**
+ * Returns a snapshot of the rank settings of this and everything it inherits
+ * Changes to the returned set will not be reflected in this rank profile.
+ */
+ public Set<RankSetting> rankSettings() {
+ Set<RankSetting> allSettings = new LinkedHashSet<>(rankSettings);
+ RankProfile parent = getInherited();
+ if (parent != null)
+ allSettings.addAll(parent.rankSettings());
+
+ return allSettings;
+ }
+
+ public void addConstant(String name, Value value) {
+ constants.put(name, value.freeze());
+ }
+
+ public void addConstantTensor(String name, TensorValue value) {
+ addConstant(name, value);
+ }
+
+ /**
+ * Returns an unmodifiable view of the constants to use in this.
+ */
+ public Map<String, Value> getConstants() {
+ if (constants.isEmpty())
+ return getInherited() != null ? getInherited().getConstants() : Collections.<String,Value>emptyMap();
+ if (getInherited() == null || getInherited().getConstants().isEmpty())
+ return Collections.unmodifiableMap(constants);
+
+ Map<String, Value> combinedConstants = new HashMap<>(getInherited().getConstants());
+ combinedConstants.putAll(constants);
+ return combinedConstants;
+ }
+
+ public void addAttributeType(String attributeName, String attributeType) {
+ attributeTypes.addType(attributeName, attributeType);
+ }
+
+ public Map<String, String> getAttributeTypes() {
+ return attributeTypes.getTypes();
+ }
+
+ public void addQueryFeatureType(String queryFeature, String queryFeatureType) {
+ queryFeatureTypes.addType(queryFeature, queryFeatureType);
+ }
+
+ public Map<String, String> getQueryFeatureTypes() {
+ return queryFeatureTypes.getTypes();
+ }
+
+ /**
+ * Returns the ranking expression to use by this. This expression must not be edited.
+ * Returns null if no expression is set.
+ */
+ public RankingExpression getFirstPhaseRanking() {
+ if (firstPhaseRanking!=null) return firstPhaseRanking;
+ if (getInherited()!=null) return getInherited().getFirstPhaseRanking();
+ return null;
+ }
+
+ public void setFirstPhaseRanking(RankingExpression rankingExpression) {
+ this.firstPhaseRanking=rankingExpression;
+ }
+
+ /**
+ * Returns the ranking expression to use by this. This expression must not be edited.
+ * Returns null if no expression is set.
+ */
+ public RankingExpression getSecondPhaseRanking() {
+ if (secondPhaseRanking!=null) return secondPhaseRanking;
+ if (getInherited()!=null) return getInherited().getSecondPhaseRanking();
+ return null;
+ }
+
+ public void setSecondPhaseRanking(RankingExpression rankingExpression) {
+ this.secondPhaseRanking=rankingExpression;
+ }
+
+ /**
+ * Called by parser to store the expression string, for delayed evaluation
+ * @param exp ranking expression for second phase
+ */
+ public void setSecondPhaseRankingString(String exp) {
+ this.secondPhaseRankingString = exp;
+ }
+
+ /**
+ * Called by parser to store the expression string, for delayed evaluation
+ * @param exp ranking expression for first phase
+ */
+ public void setFirstPhaseRankingString(String exp) {
+ this.firstPhaseRankingString = exp;
+ }
+
+ /** Returns a read-only view of the summary features to use in this profile. This is never null */
+ public Set<ReferenceNode> getSummaryFeatures() {
+ if (summaryFeatures!=null) return Collections.unmodifiableSet(summaryFeatures);
+ if (getInherited()!=null) return getInherited().getSummaryFeatures();
+ return Collections.emptySet();
+ }
+
+ public void addSummaryFeature(ReferenceNode feature) {
+ if (summaryFeatures==null)
+ summaryFeatures=new LinkedHashSet<>();
+ summaryFeatures.add(feature);
+ }
+
+ /**
+ * Adds the content of the given feature list to the internal list of summary features.
+ *
+ * @param features The features to add.
+ */
+ public void addSummaryFeatures(FeatureList features) {
+ for (ReferenceNode feature : features) {
+ addSummaryFeature(feature);
+ }
+ }
+
+ /** Returns a read-only view of the rank features to use in this profile. This is never null */
+ public Set<ReferenceNode> getRankFeatures() {
+ if (rankFeatures != null) return Collections.unmodifiableSet(rankFeatures);
+ if (getInherited() != null) return getInherited().getRankFeatures();
+ return Collections.emptySet();
+ }
+
+ public void addRankFeature(ReferenceNode feature) {
+ if (rankFeatures==null)
+ rankFeatures=new LinkedHashSet<>();
+ rankFeatures.add(feature);
+ }
+
+ /**
+ * Adds the content of the given feature list to the internal list of rank features.
+ *
+ * @param features The features to add.
+ */
+ public void addRankFeatures(FeatureList features) {
+ for (ReferenceNode feature : features) {
+ addRankFeature(feature);
+ }
+ }
+
+ /** Returns a read only flattened list view of the rank properties to use in this profile. This is never null. */
+ public List<RankProperty> getRankProperties() {
+ List<RankProperty> properties = new ArrayList<>();
+ for (List<RankProperty> propertyList : getRankPropertyMap().values()) {
+ properties.addAll(propertyList);
+ }
+ return Collections.unmodifiableList(properties);
+ }
+
+ /** Returns a read only map view of the rank properties to use in this profile. This is never null. */
+ public Map<String, List<RankProperty>> getRankPropertyMap() {
+ if (rankProperties.size() == 0 && getInherited() == null) return Collections.emptyMap();
+ if (rankProperties.size() == 0) return getInherited().getRankPropertyMap();
+ if (getInherited() == null) return Collections.unmodifiableMap(rankProperties);
+
+ // Neither is null
+ Map<String, List<RankProperty>> combined = new LinkedHashMap<>(getInherited().getRankPropertyMap());
+ combined.putAll(rankProperties); // Don't combine values across inherited properties
+ return Collections.unmodifiableMap(combined);
+ }
+
+ public void addRankProperty(String name, String parameter) {
+ addRankProperty(new RankProperty(name, parameter));
+ }
+
+ public void addRankProperty(RankProperty rankProperty) {
+ // Just the usual multimap semantics here
+ List<RankProperty> properties = rankProperties.get(rankProperty.getName());
+ if (properties == null) {
+ properties = new ArrayList<>(1);
+ rankProperties.put(rankProperty.getName(), properties);
+ }
+ properties.add(rankProperty);
+ }
+
+ public String toString() {
+ return "rank profile " + getName();
+ }
+
+ public int getRerankCount() {
+ if (rerankCount>=0) return rerankCount;
+ if (getInherited()!=null) return getInherited().getRerankCount();
+ return -1;
+ }
+
+ public int getNumThreadsPerSearch() {
+ return numThreadsPerSearch;
+ }
+
+ public void setNumThreadsPerSearch(int numThreads) {
+ this.numThreadsPerSearch = numThreads;
+ }
+
+ public void setNumSearchPartitions(int numSearchPartitions) {
+ this.numSearchPartitions = numSearchPartitions;
+ }
+
+ public int getNumSearchPartitions() { return numSearchPartitions; }
+
+ public double getTermwiseLimit() { return termwiseLimit; }
+ public void setTermwiseLimit(double termwiseLimit) { this.termwiseLimit = termwiseLimit; }
+
+ /** Sets the rerank count. Set to -1 to use inherited */
+ public void setRerankCount(int rerankCount) {
+ this.rerankCount = rerankCount;
+ }
+
+ /** Whether we should ignore the default rank features. Set to null to use inherited */
+ public void setIgnoreDefaultRankFeatures(Boolean ignoreDefaultRankFeatures) {
+ this.ignoreDefaultRankFeatures = ignoreDefaultRankFeatures;
+ }
+
+ public boolean getIgnoreDefaultRankFeatures() {
+ if (ignoreDefaultRankFeatures!=null) return ignoreDefaultRankFeatures;
+ return (getInherited()!=null) && getInherited().getIgnoreDefaultRankFeatures();
+ }
+
+ /**
+ * Returns the string form of the second phase ranking expression.
+ * @return string form of second phase ranking expression
+ */
+ public String getSecondPhaseRankingString() {
+ if (secondPhaseRankingString != null) return secondPhaseRankingString;
+ if (getInherited() != null) return getInherited().getSecondPhaseRankingString();
+ return null;
+ }
+
+ /**
+ * Returns the string form of the first phase ranking expression.
+ * @return string form of first phase ranking expression
+ */
+ public String getFirstPhaseRankingString() {
+ if (firstPhaseRankingString != null) return firstPhaseRankingString;
+ if (getInherited() != null) return getInherited().getFirstPhaseRankingString();
+ return null;
+ }
+
+ public void addMacro(String name, boolean inline) {
+ macros.put(name, new Macro(name, inline));
+ }
+
+ /** Returns an unmodifiable view of the macros in this */
+ public Map<String, Macro> getMacros() {
+ if (macros.size() == 0 && getInherited()==null) return Collections.emptyMap();
+ if (macros.size() == 0) return getInherited().getMacros();
+ if (getInherited() == null) return Collections.unmodifiableMap(macros);
+
+ // Neither is null
+ Map<String, Macro> allMacros = new LinkedHashMap<>(getInherited().getMacros());
+ allMacros.putAll(macros);
+ return Collections.unmodifiableMap(allMacros);
+
+ }
+
+ public int getKeepRankCount() {
+ if (keepRankCount>=0) return keepRankCount;
+ if (getInherited()!=null) return getInherited().getKeepRankCount();
+ return -1;
+ }
+
+ public void setKeepRankCount(int rerankArraySize) {
+ this.keepRankCount = rerankArraySize;
+ }
+
+ public double getRankScoreDropLimit() {
+ if (rankScoreDropLimit>-Double.MAX_VALUE) return rankScoreDropLimit;
+ if (getInherited()!=null) return getInherited().getRankScoreDropLimit();
+ return rankScoreDropLimit;
+ }
+
+ public void setRankScoreDropLimit(double rankScoreDropLimit) {
+ this.rankScoreDropLimit = rankScoreDropLimit;
+ }
+
+ public Set<String> filterFields() {
+ return filterFields;
+ }
+
+ /**
+ * Returns all filter fields in this profile and any profile it inherits.
+ * @return the set of all filter fields
+ */
+ public Set<String> allFilterFields() {
+ RankProfile parent = getInherited();
+ Set<String> retval = new LinkedHashSet<>();
+ if (parent != null) {
+ retval.addAll(parent.allFilterFields());
+ }
+ retval.addAll(filterFields());
+ return retval;
+ }
+
+ /**
+ * Will take the parser-set textual ranking expressions and turn into objects
+ */
+ public void parseExpressions() {
+ try {
+ parseRankingExpressions();
+ parseMacros();
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Passes the contents of macros on to parser. Then put all the implied rank properties
+ * from those macros into the profile's props map.
+ */
+ private void parseMacros() throws ParseException {
+ for (Map.Entry<String, Macro> e : getMacros().entrySet()) {
+ String macroName = e.getKey();
+ Macro macro = e.getValue();
+ RankingExpression expr = parseRankingExpression(macroName, macro.getTextualExpression());
+ macro.setRankingExpression(expr);
+ macro.setTextualExpression(expr.getRoot().toString());
+ }
+ }
+
+ /**
+ * Passes ranking expressions on to parser
+ * @throws ParseException if either of the ranking expressions could not be parsed
+ */
+ private void parseRankingExpressions() throws ParseException {
+ if (getFirstPhaseRankingString() != null)
+ setFirstPhaseRanking(parseRankingExpression("firstphase", getFirstPhaseRankingString()));
+ if (getSecondPhaseRankingString() != null)
+ setSecondPhaseRanking(parseRankingExpression("secondphase", getSecondPhaseRankingString()));
+ }
+
+ private RankingExpression parseRankingExpression(String expName, String exp) throws ParseException {
+ if (exp.trim().length() == 0)
+ throw new ParseException("Encountered an empty ranking expression in " + getName()+ ", " + expName + ".");
+
+ try {
+ RankingExpression expression = new RankingExpression(openRankingExpressionReader(expName, exp.trim()));
+ expression.setName(expName);
+ return expression;
+ }
+ catch (com.yahoo.searchlib.rankingexpression.parser.ParseException e) {
+ ParseException exception = new ParseException("Could not parse ranking expression '" + exp.trim() +
+ "' in " + getName()+ ", " + expName + ".");
+ throw (ParseException)exception.initCause(e);
+ }
+ }
+
+ private Reader openRankingExpressionReader(String expName, String expression) {
+ if ( ! expression.startsWith("file:")) return new StringReader(expression);
+
+ String fileName = expression.substring("file:".length()).trim();
+ if ( ! fileName.endsWith(ApplicationPackage.RANKEXPRESSION_NAME_SUFFIX))
+ fileName = fileName + ApplicationPackage.RANKEXPRESSION_NAME_SUFFIX;
+
+ final File file = new File(fileName);
+ if ( ! (file.isAbsolute()) && file.getPath().contains("/")) // See ticket 4102122
+ throw new IllegalArgumentException("In " + getName() +", " + expName + ", ranking references file '" + file +
+ "' in subdirectory, which is not supported.");
+
+ return search.getRankingExpression(fileName);
+ }
+
+ /** Shallow clones this */
+ @Override
+ public RankProfile clone() {
+ try {
+ // Note: This treats RankingExpression in Macros as immutables even though they are not
+ RankProfile clone = (RankProfile)super.clone();
+ clone.rankSettings = new LinkedHashSet<>(this.rankSettings);
+ clone.matchPhaseSettings = this.matchPhaseSettings; // hmm?
+ clone.summaryFeatures = summaryFeatures != null ? new LinkedHashSet<>(this.summaryFeatures) : null;
+ clone.rankFeatures = rankFeatures != null ? new LinkedHashSet<>(this.rankFeatures) : null;
+ clone.rankProperties = new LinkedHashMap<>(this.rankProperties);
+ clone.macros = new LinkedHashMap<>(this.macros);
+ clone.filterFields = new HashSet<>(this.filterFields);
+ clone.constants = new HashMap<>(this.constants);
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Won't happen", e);
+ }
+ }
+
+ /**
+ * Returns a copy of this where the content is optimized for execution.
+ * Compiled profiles should never be modified.
+ */
+ public RankProfile compile() {
+ try {
+ RankProfile compiled = this.clone();
+ compiled.compileThis();
+ return compiled;
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Rank profile '" + getName() + "' is invalid", e);
+ }
+ }
+
+ private void compileThis() {
+ parseExpressions();
+
+ checkNameCollisions(getMacros(), getConstants());
+
+ Map<String, Macro> compiledMacros = new LinkedHashMap<>();
+ for (Map.Entry<String, Macro> macroEntry : getMacros().entrySet()) {
+ Macro compiledMacro = macroEntry.getValue().clone();
+ compiledMacro.setRankingExpression(compile(macroEntry.getValue().getRankingExpression(),
+ getConstants(), Collections.<String, Macro>emptyMap()));
+ compiledMacros.put(macroEntry.getKey(), compiledMacro);
+ }
+ macros = compiledMacros;
+ Map<String, Macro> inlineMacros = keepInline(compiledMacros);
+ firstPhaseRanking = compile(this.getFirstPhaseRanking(), getConstants(), inlineMacros);
+ secondPhaseRanking = compile(this.getSecondPhaseRanking(), getConstants(), inlineMacros);
+ }
+
+ private void checkNameCollisions(Map<String, Macro> macros, Map<String, Value> constants) {
+ for (Map.Entry<String, Macro> macroEntry : macros.entrySet()) {
+ if (constants.get(macroEntry.getKey()) != null)
+ throw new IllegalArgumentException("Cannot have both a constant and macro named '" + macroEntry.getKey() + "'");
+ }
+ }
+
+ private Map<String, Macro> keepInline(Map<String, Macro> macros) {
+ Map<String, Macro> inlineMacros = new HashMap<>();
+ for (Map.Entry<String, Macro> entry : macros.entrySet())
+ if (entry.getValue().getInline())
+ inlineMacros.put(entry.getKey(), entry.getValue());
+ return inlineMacros;
+ }
+
+ private RankingExpression compile(RankingExpression expression,
+ Map<String, Value> constants,
+ Map<String, Macro> inlineMacros) {
+ if (expression == null) return null;
+ Map<String, String> rankPropertiesOutput = new HashMap<>();
+ expression = new ConstantDereferencer(constants).transform(expression);
+ expression = new ConstantTensorTransformer(constants, rankPropertiesOutput).transform(expression);
+ expression = new MacroInliner(inlineMacros).transform(expression);
+ expression = new Simplifier().transform(expression);
+ for (Map.Entry<String, String> rankProperty : rankPropertiesOutput.entrySet()) {
+ addRankProperty(rankProperty.getKey(), rankProperty.getValue());
+ }
+ return expression;
+ }
+
+ /**
+ * A rank setting. The identity of a rank setting is its field name and type (not value).
+ * A rank setting is immutable.
+ */
+ public static class RankSetting implements Serializable {
+
+ private String fieldName;
+
+ private Type type;
+
+ /** The rank value */
+ private Object value;
+
+ public enum Type {
+
+ RANKTYPE("rank-type"),
+ LITERALBOOST("literal-boost"),
+ WEIGHT("weight"),
+ PREFERBITVECTOR("preferbitvector",true);
+
+ private String name;
+
+ /** True if this setting really pertains to an index, not a field within an index */
+ private boolean isIndexLevel;
+
+ private Type(String name) {
+ this(name,false);
+ }
+
+ private Type(String name,boolean isIndexLevel) {
+ this.name = name;
+ this.isIndexLevel=isIndexLevel;
+ }
+
+ /** True if this setting really pertains to an index, not a field within an index */
+ public boolean isIndexLevel() { return isIndexLevel; }
+
+ /** @return The name of this type */
+ public String getName() {
+ return name;
+ }
+
+ public String toString() {
+ return "type: " + name;
+ }
+
+ }
+
+ public RankSetting(String fieldName, RankSetting.Type type, Object value) {
+ this.fieldName = fieldName;
+ this.type = type;
+ this.value = value;
+ }
+
+ public String getFieldName() { return fieldName; }
+
+ public Type getType() { return type; }
+
+ public Object getValue() { return value; }
+
+ /** @return The value as an int, or a negative value if it is not an integer */
+ public int getIntValue() {
+ if (value instanceof Integer) {
+ return ((Integer)value);
+ }
+ else {
+ return -1;
+ }
+ }
+
+ public int hashCode() {
+ return fieldName.hashCode() + 17 * type.hashCode();
+ }
+
+ public boolean equals(Object object) {
+ if (!(object instanceof RankSetting)) {
+ return false;
+ }
+ RankSetting other = (RankSetting)object;
+ return
+ fieldName.equals(other.fieldName) &&
+ type.equals(other.type);
+ }
+
+ public String toString() {
+ return type + " setting " + fieldName + ": " + value;
+ }
+
+ }
+
+ /** A rank property. Rank properties are Value Objects */
+ public static class RankProperty implements Serializable {
+
+ private String name;
+ private String value;
+
+ public RankProperty(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() { return name; }
+
+ public String getValue() { return value; }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() + 17*value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (! (object instanceof RankProperty)) return false;
+ RankProperty other=(RankProperty)object;
+ return (other.name.equals(this.name) && other.value.equals(this.value));
+ }
+
+ @Override
+ public String toString() {
+ return name + " = " + value;
+ }
+
+ }
+
+ /**
+ * Represents a declared macro in the profile. It is, after parsing, transformed into ExpressionMacro
+ *
+ * @author vegardh
+ */
+ public static class Macro implements Serializable, Cloneable {
+
+ private String name=null;
+ private String textualExpression=null;
+ private RankingExpression expression=null;
+ private List<String> formalParams = new ArrayList<>();
+
+ /** True if this should be inlined into calling expressions. Useful for very cheap macros. */
+ private final boolean inline;
+
+ public Macro(String name, boolean inline) {
+ this.name = name;
+ this.inline = inline;
+ }
+
+ public void addParam(String name) {
+ formalParams.add(name);
+ }
+
+ public List<String> getFormalParams() {
+ return formalParams;
+ }
+
+ public String getTextualExpression() {
+ return textualExpression;
+ }
+
+ public void setTextualExpression(String textualExpression) {
+ this.textualExpression = textualExpression;
+ }
+
+ public void setRankingExpression(RankingExpression expr) {
+ this.expression=expr;
+ }
+
+ public RankingExpression getRankingExpression() {
+ return expression;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public boolean getInline() {
+ return inline && formalParams.size() == 0; // only inline no-arg macros;
+ }
+
+ public ExpressionFunction toExpressionMacro() {
+ return new ExpressionFunction(getName(), getFormalParams(), getRankingExpression());
+ }
+
+ @Override
+ public Macro clone() {
+ try {
+ return (Macro)super.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Won't happen", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "macro " + getName() + ": " + expression;
+ }
+
+ }
+
+ public static final class DiversitySettings {
+ private String attribute = null;
+ private int minGroups = 0;
+ private double cutoffFactor = 10;
+ private Diversity.CutoffStrategy cutoffStrategy = Diversity.CutoffStrategy.loose;
+
+ public void setAttribute(String value) { attribute = value; }
+ public void setMinGroups(int value) { minGroups = value; }
+ public void setCutoffFactor(double value) { cutoffFactor = value; }
+ public void setCutoffStrategy(Diversity.CutoffStrategy strategy) { cutoffStrategy = strategy; }
+ public void setCutoffStrategy(String strategy) { cutoffStrategy = Diversity.CutoffStrategy.valueOf(strategy); }
+ public String getAttribute() { return attribute; }
+ public int getMinGroups() { return minGroups; }
+ public double getCutoffFactor() { return cutoffFactor; }
+ public Diversity.CutoffStrategy getCutoffStrategy() { return cutoffStrategy; }
+ public void checkValid() {
+ if (attribute == null || attribute.isEmpty()) {
+ throw new IllegalArgumentException("'diversity' did not set non-empty diversity attribute name.");
+ }
+ if (minGroups <= 0) {
+ throw new IllegalArgumentException("'diversity' did not set min-groups > 0");
+ }
+ if (cutoffFactor < 1.0) {
+ throw new IllegalArgumentException("diversity.cutoff.factor must be larger or equal to 1.0.");
+ }
+ }
+ }
+
+ public static class MatchPhaseSettings {
+ private String attribute = null;
+ private boolean ascending = false;
+ private int maxHits = 0; // try to get this many hits before degrading the match phase
+ private double maxFilterCoverage = 1.0; // Max coverage of original corpus that will trigger the filter.
+ private DiversitySettings diversity = null;
+ private double evaluationPoint = 0.20;
+ private double prePostFilterTippingPoint = 1.0;
+
+ public void setDiversity(DiversitySettings value) {
+ value.checkValid();
+ diversity = value;
+ }
+ public void setAscending(boolean value) { ascending = value; }
+ public void setAttribute(String value) { attribute = value; }
+ public void setMaxHits(int value) { maxHits = value; }
+ public void setMaxFilterCoverage(double value) { maxFilterCoverage = value; }
+ public void setEvaluationPoint(double evaluationPoint) { this.evaluationPoint = evaluationPoint; }
+ public void setPrePostFilterTippingPoint(double prePostFilterTippingPoint) { this.prePostFilterTippingPoint = prePostFilterTippingPoint; }
+
+ public boolean getAscending() { return ascending; }
+ public String getAttribute() { return attribute; }
+ public int getMaxHits() { return maxHits; }
+ public double getMaxFilterCoverage() { return maxFilterCoverage; }
+ public DiversitySettings getDiversity() { return diversity; }
+ public double getEvaluationPoint() { return evaluationPoint; }
+ public double getPrePostFilterTippingPoint() { return prePostFilterTippingPoint; }
+
+ public void checkValid() {
+ if (attribute == null) {
+ throw new IllegalArgumentException("match-phase did not set any attribute");
+ }
+ if (! (maxHits > 0)) {
+ throw new IllegalArgumentException("match-phase did not set max-hits > 0");
+ }
+ }
+
+ }
+
+ public static class TypeSettings {
+
+ private final Map<String, String> types = new HashMap<>();
+
+ public void addType(String name, String type) {
+ types.put(name, type);
+ }
+
+ public Map<String, String> getTypes() {
+ return Collections.unmodifiableMap(types);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/RankProfileRegistry.java b/config-model/src/main/java/com/yahoo/searchdefinition/RankProfileRegistry.java
new file mode 100644
index 00000000000..a9ee3c3cc5c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/RankProfileRegistry.java
@@ -0,0 +1,90 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import java.util.*;
+
+/**
+ * Mapping from name to {@link RankProfile} as well as a reverse mapping of {@link RankProfile} to {@link Search}.
+ * Having both of these mappings consolidated here will make it easier to remove dependencies on these mappings at
+ * run time, since it is essentially only used when building rank profile config at deployment time.
+ *
+ * TODO: Reconsider the difference between local and global maps. Right now, the local maps might better be
+ * served from a different class owned by SearchBuilder.
+ *
+ * @author lulf
+ * @since 5.20
+ */
+public class RankProfileRegistry {
+
+ private final Map<RankProfile, Search> rankProfileToSearch = new LinkedHashMap<>();
+ private final Map<Search, Map<String, RankProfile>> rankProfiles = new LinkedHashMap<>();
+ /* These rank profiles can be overridden: 'default' rank profile, as that is documented to work. And 'unranked'. */
+ static final Set<String> overridableRankProfileNames = new HashSet<>(Arrays.asList("default", "unranked"));
+
+ public RankProfileRegistry() {
+ }
+
+ public static RankProfileRegistry createRankProfileRegistryWithBuiltinRankProfiles(Search search) {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ rankProfileRegistry.addRankProfile(new DefaultRankProfile(search, rankProfileRegistry));
+ rankProfileRegistry.addRankProfile(new UnrankedRankProfile(search, rankProfileRegistry));
+ return rankProfileRegistry;
+ }
+
+ /**
+ * Adds a rank profile to this registry
+ *
+ * @param rankProfile the rank profile to add
+ */
+ public void addRankProfile(RankProfile rankProfile) {
+ if (!rankProfiles.containsKey(rankProfile.getSearch())) {
+ rankProfiles.put(rankProfile.getSearch(), new LinkedHashMap<>());
+ }
+ checkForDuplicateRankProfile(rankProfile);
+ rankProfiles.get(rankProfile.getSearch()).put(rankProfile.getName(), rankProfile);
+ rankProfileToSearch.put(rankProfile, rankProfile.getSearch());
+ }
+
+ private void checkForDuplicateRankProfile(RankProfile rankProfile) {
+ final String rankProfileName = rankProfile.getName();
+ RankProfile existingRangProfileWithSameName = rankProfiles.get(rankProfile.getSearch()).get(rankProfileName);
+ if (existingRangProfileWithSameName == null) return;
+
+ if (!overridableRankProfileNames.contains(rankProfileName)) {
+ throw new IllegalArgumentException("Cannot add rank profile '" + rankProfileName + "' in search definition '"
+ + rankProfile.getSearch().getName() + "', since it already exists");
+ }
+ }
+
+ /**
+ * Returns a named rank profile, null if the search definition doesn't have one with the given name
+ *
+ * @param search The {@link Search} that owns the rank profile.
+ * @param name The name of the rank profile
+ * @return The RankProfile to return.
+ */
+ public RankProfile getRankProfile(Search search, String name) {
+ return rankProfiles.get(search).get(name);
+ }
+
+ /**
+ * Rank profiles that are collected across clusters.
+ * @return A set of global {@link RankProfile} instances.
+ */
+ public Set<RankProfile> allRankProfiles() {
+ return rankProfileToSearch.keySet();
+ }
+
+ /**
+ * Rank profiles that are collected for a given search definition
+ * @param search {@link Search} to get rank profiles for.
+ * @return A collection of local {@link RankProfile} instances.
+ */
+ public Collection<RankProfile> localRankProfiles(Search search) {
+ Map<String, RankProfile> mapping = rankProfiles.get(search);
+ if (mapping == null) {
+ return Collections.emptyList();
+ }
+ return mapping.values();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/SDDocumentTypeOrderer.java b/config-model/src/main/java/com/yahoo/searchdefinition/SDDocumentTypeOrderer.java
new file mode 100644
index 00000000000..7d8a87cee8c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/SDDocumentTypeOrderer.java
@@ -0,0 +1,126 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.document.*;
+import com.yahoo.document.annotation.AnnotationReferenceDataType;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.TemporarySDDocumentType;
+
+import java.util.*;
+import java.util.logging.Level;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class SDDocumentTypeOrderer {
+ private Map<DataTypeName, SDDocumentType> createdSDTypes = new LinkedHashMap<>();
+ private Set<Integer> seenTypes = new LinkedHashSet<>();
+ List<SDDocumentType> processingOrder = new LinkedList<>();
+ private DeployLogger deployLogger;
+
+ public SDDocumentTypeOrderer(List<SDDocumentType> sdTypes, DeployLogger deployLogger) {
+ this.deployLogger = deployLogger;
+ for (SDDocumentType type : sdTypes) {
+ createdSDTypes.put(type.getDocumentName(), type);
+ }
+ DocumentTypeManager dtm = new DocumentTypeManager();
+ for (DataType type : dtm.getDataTypes()) {
+ seenTypes.add(type.getId());
+ }
+
+ }
+
+ List<SDDocumentType> getOrdered() { return processingOrder; }
+
+ public void process() {
+ for (SDDocumentType type : createdSDTypes.values()) {
+ process(type);
+ }
+ }
+ private void process(SDDocumentType type) {
+ List<DataTypeName> toReplace = new ArrayList<>();
+ for (SDDocumentType sdoc : type.getInheritedTypes()) {
+ if (sdoc instanceof TemporarySDDocumentType) {
+ toReplace.add(sdoc.getDocumentName());
+ }
+ }
+ for (DataTypeName name : toReplace) {
+ SDDocumentType inherited = createdSDTypes.get(name);
+ if (inherited == null) {
+ throw new IllegalStateException("Document type '" + name + "' not found.");
+ }
+ process(inherited);
+ type.inherit(inherited);
+ }
+ visit(type);
+ }
+
+ private void visit(SDDocumentType docOrStruct) {
+ int id;
+ if (docOrStruct.isStruct()) {
+ id = new StructDataType(docOrStruct.getName()).getId();
+ } else {
+ id = new DocumentType(docOrStruct.getName()).getId();
+ }
+
+ if (seenTypes.contains(id)) {
+ return;
+ } else {
+ seenTypes.add((new StructDataType(docOrStruct.getName()).getId()));
+ }
+
+
+ for (Field field : docOrStruct.fieldSet()) {
+ if (!seenTypes.contains(field.getDataType().getId())) {
+ //we haven't seen this before, do it
+ visit(field.getDataType());
+ }
+ }
+ processingOrder.add(docOrStruct);
+ }
+
+ private SDDocumentType find(String name) {
+ SDDocumentType sdDocType = createdSDTypes.get(new DataTypeName(name));
+ if (sdDocType != null) {
+ return sdDocType;
+ }
+ for(SDDocumentType sdoc : createdSDTypes.values()) {
+ for (SDDocumentType stype : sdoc.getTypes()) {
+ if (stype.getName().equals(name)) {
+ return stype;
+ }
+ }
+ }
+ return null;
+ }
+ private void visit(DataType type) {
+ if (type instanceof StructuredDataType) {
+ StructuredDataType structType = (StructuredDataType) type;
+ SDDocumentType sdDocType = find(structType.getName());
+ if (sdDocType == null) {
+ throw new IllegalArgumentException("Could not find struct '" + type.getName() + "'.");
+ }
+ visit(sdDocType);
+ return;
+ }
+
+ if (type instanceof MapDataType) {
+ MapDataType mType = (MapDataType) type;
+ visit(mType.getValueType());
+ visit(mType.getKeyType());
+ } else if (type instanceof WeightedSetDataType) {
+ WeightedSetDataType wType = (WeightedSetDataType) type;
+ visit(wType.getNestedType());
+ } else if (type instanceof CollectionDataType) {
+ CollectionDataType cType = (CollectionDataType) type;
+ visit(cType.getNestedType());
+ } else if (type instanceof AnnotationReferenceDataType) {
+ //do nothing
+ } else if (type instanceof PrimitiveDataType) {
+ //do nothing
+ } else {
+ deployLogger.log(Level.WARNING, "Unknown type : " + type);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/Search.java b/config-model/src/main/java/com/yahoo/searchdefinition/Search.java
new file mode 100644
index 00000000000..43c6bb4b441
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/Search.java
@@ -0,0 +1,555 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.document.*;
+import com.yahoo.searchdefinition.document.annotation.SDAnnotationType;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+
+import java.io.Reader;
+import java.io.Serializable;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * <p>A search definition describes (or uses) some document types, defines how these are turned into a relevancy tuned
+ * index through indexing and how data from documents should be served at search time.</p> <p>The identity of this
+ * class is its name.</p>
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+// TODO: Make a class owned by this, for each of these responsibilities:
+// Managing indexes, managing attributes, managing summary classes.
+// Ensure that after the processing step, all implicit instances of the above types are explicitly represented
+public class Search implements Serializable {
+
+ private static final Logger log = Logger.getLogger(Search.class.getName());
+ private static final String SD_DOC_FIELD_NAME = "sddocname";
+ private static final List<String> RESERVED_NAMES = Arrays.asList(
+ "index", "index_url", "summary", "attribute", "select_input", "host", "documentid",
+ "position", "split_foreach", "tokenize", "if", "else", "switch", "case", SD_DOC_FIELD_NAME, "relevancy");
+
+ /**
+ * @return True if the given field name is a reserved name.
+ */
+ public static boolean isReservedName(String name) {
+ return RESERVED_NAMES.contains(name);
+ }
+
+ // Field sets
+ private FieldSets fieldSets = new FieldSets();
+
+ // Whether or not this object has been processed.
+ private boolean processed;
+
+ // The unique name of this search definition.
+ private String name;
+
+ // True if this doesn't define a search, just some documents.
+ private boolean documentsOnly = false;
+
+ // The stemming setting of this search definition. Default is SHORTEST.
+ private Stemming stemming = Stemming.SHORTEST;
+
+ // Documents contained in this definition.
+ private SDDocumentType docType;
+
+ // The extra fields of this search definition.
+ private Map<String, SDField> fields = new LinkedHashMap<>();
+
+ // The explicitly defined indices of this search definition.
+ private Map<String, Index> indices = new LinkedHashMap<>();
+
+ // The explicitly defined summaries of this search definition.
+ private Map<String, DocumentSummary> summaries = new LinkedHashMap<>();
+ // _Must_ preserve order
+
+ private ApplicationPackage sourceApplication;
+
+ /**
+ * Creates a search definition which just holds a set of documents which should not (here, directly) be searchable
+ */
+ protected Search() {
+ documentsOnly = true;
+ }
+
+ /**
+ * Creates a proper search definition
+ * @param name of the the searchdefinition
+ * @param sourceApplication the application containing this
+ */
+ public Search(String name, ApplicationPackage sourceApplication) {
+ this.sourceApplication = sourceApplication;
+ this.name = name;
+ }
+
+ protected void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns true if this doesn't define a search, just some documents
+ *
+ * @return if the searchdefinition only has documents
+ */
+ public boolean isDocumentsOnly() {
+ return documentsOnly;
+ }
+
+ /**
+ * Sets the stemming default of fields. Default is ALL
+ *
+ * @param stemming set default stemming for this searchdefinition
+ * @throws NullPointerException if this is attempted set to null
+ */
+ public void setStemming(Stemming stemming) {
+ if (stemming == null) {
+ throw new NullPointerException("The stemming setting of a search definition " +
+ "can not be null");
+ }
+ this.stemming = stemming;
+ }
+
+ /**
+ * Returns whether fields should be stemmed by default or not. Default is ALL. This is never null.
+ *
+ * @return the default stemming for this searchdefinition
+ */
+ public Stemming getStemming() {
+ return stemming;
+ }
+
+ /**
+ * Adds a document type which is defined in this search definition
+ *
+ * @param document the document type to add
+ */
+ public void addDocument(SDDocumentType document) {
+ if (docType != null) {
+ throw new IllegalArgumentException("Searchdefinition cannot have more than one document");
+ }
+ docType = document;
+ }
+
+ /**
+ * Gets a document from this search definition
+ *
+ * @param name the name of the document to return
+ * @return the contained or used document type, or null if there is no such document
+ */
+ public SDDocumentType getDocument(String name) {
+ if (docType != null && name.equals(docType.getName())) {
+ return docType;
+ }
+ return null;
+ }
+
+ /**
+ * @return true if the document has been added.
+ */
+ public boolean hasDocument() {
+ return docType != null;
+ }
+
+ /**
+ * @return The document in this search.
+ */
+ public SDDocumentType getDocument() {
+ return docType;
+ }
+
+ /**
+ * Returns a list of all the fields of this search definition, that is all fields in all documents, in the documents
+ * they inherit, and all extra fields. The caller receives ownership to the list - subsequent changes to it will not
+ * impact this Search
+ *
+ * @return the list of fields in this searchdefinition
+ */
+ public List<SDField> allFieldsList() {
+ List<SDField> allFields = new ArrayList<>();
+ allFields.addAll(extraFieldList());
+ for (Field field : docType.fieldSet()) {
+ allFields.add((SDField)field);
+ }
+ return allFields;
+ }
+
+ /**
+ * Returns the content of a ranking expression file
+ */
+ public Reader getRankingExpression(String fileName) {
+ return sourceApplication.getRankingExpression(fileName);
+ }
+
+ /**
+ * Returns a field defined in this search definition or one if its documents. Fields in this search definition takes
+ * precedence over document fields having the same name
+ *
+ * @param name of the field
+ * @return the SDField representing the field
+ */
+ public SDField getField(String name) {
+ SDField field = getExtraField(name);
+ if (field != null) {
+ return field;
+ }
+ return (SDField)docType.getField(name);
+ }
+
+ /**
+ * Returns a field defined in one of the documents of this search definition. This does <b>not</b> include the extra
+ * fields defined outside of a document (those accessible through the getExtraField() method).
+ *
+ * @param name The name of the field to return.
+ * @return The named field, or null if not found.
+ */
+ public SDField getDocumentField(String name) {
+ return (SDField)docType.getField(name);
+ }
+
+ /**
+ * Adds an extra field of this search definition not contained in a document
+ *
+ * @param field to add to the searchdefinitions list of external fields.
+ */
+ public void addExtraField(SDField field) {
+ if (fields.containsKey(field.getName())) {
+ log.warning("Duplicate field " + field.getName() + " in search definition " + getName());
+ } else {
+ field.setIsExtraField(true);
+ fields.put(field.getName(), field);
+ }
+ }
+
+ public Collection<SDField> extraFieldList() {
+ return fields.values();
+ }
+ public Collection<SDField> allExtraFields() {
+ Map<String, SDField> extraFields = new TreeMap<>();
+ for (Field field : docType.fieldSet()) {
+ SDField sdField = (SDField) field;
+ if (sdField.isExtraField()) {
+ extraFields.put(sdField.getName(), sdField);
+ }
+ }
+ for (SDField field : extraFieldList()) {
+ extraFields.put(field.getName(), field);
+ }
+ return extraFields.values();
+ }
+
+ /**
+ * Returns a field by name, or null if it is not present
+ *
+ * @param fieldName the name of the external field to get
+ * @return the SDField of this name
+ */
+ public SDField getExtraField(String fieldName) {
+ return fields.get(fieldName);
+ }
+
+ /**
+ * Adds an explicitly defined index to this search definition
+ *
+ * @param index the index to add
+ */
+ public void addIndex(Index index) {
+ indices.put(index.getName(), index);
+ }
+
+ /**
+ * <p>Returns an index, or null if no index with this name has had some <b>explicit settings</b> applied. Even if
+ * this returns null, the index may be implicitly defined by an indexing statement.</p>
+ * <p>This will return the
+ * index whether it is defined on this search or on one of its fields</p>
+ *
+ * @param name the name of the index to get
+ * @return the index requested
+ */
+ public Index getIndex(String name) {
+ List<Index> sameIndices = new ArrayList<>(1);
+ Index searchIndex = indices.get(name);
+ if (searchIndex != null) {
+ sameIndices.add(searchIndex);
+ }
+
+ for (SDField field : allFieldsList()) {
+ Index index = field.getIndex(name);
+ if (index != null) {
+ sameIndices.add(index);
+ }
+ }
+ if (sameIndices.size() == 0) {
+ return null;
+ }
+ if (sameIndices.size() == 1) {
+ return sameIndices.get(0);
+ }
+ return consolidateIndices(sameIndices);
+ }
+
+ public boolean existsIndex(String name) {
+ if (indices.get(name) != null) {
+ return true;
+ }
+ for (SDField field : allFieldsList()) {
+ if (field.existsIndex(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Consolidates a set of index settings for the same index into one
+ *
+ * @param indices The list of indexes to consolidate.
+ * @return The consolidated index
+ */
+ private Index consolidateIndices(List<Index> indices) {
+ Index first = indices.get(0);
+ Index consolidated = new Index(first.getName());
+ consolidated.setRankType(first.getRankType());
+ consolidated.setType(first.getType());
+ for (Index current : indices) {
+ if (current.isPrefix()) {
+ consolidated.setPrefix(true);
+ }
+
+ if (consolidated.getRankType() == null) {
+ consolidated.setRankType(current.getRankType());
+ } else {
+ if (current.getRankType() != null &&
+ !consolidated.getRankType().equals(current.getRankType()))
+ {
+ log.warning("Conflicting rank type settings for " +
+ first.getName() + " in " + this + ", using " +
+ consolidated.getRankType());
+ }
+ }
+
+ for (Iterator<String> j = current.aliasIterator(); j.hasNext();) {
+ consolidated.addAlias(j.next());
+ }
+ }
+ return consolidated;
+ }
+
+ /**
+ * All explicitly defined indices, both on this search definition itself (returned first) and all its fields
+ *
+ * @return The list of explicit defined indexes.
+ */
+ public List<Index> getExplicitIndices() {
+ List<Index> allIndices = new ArrayList<>(indices.values());
+ for (SDField field : allFieldsList()) {
+ for (Index index : field.getIndices().values()) {
+ allIndices.add(index);
+ }
+ }
+ return Collections.unmodifiableList(allIndices);
+ }
+
+ /**
+ * Adds an explicitly defined summary to this search definition
+ *
+ * @param summary The summary to add.
+ */
+ public void addSummary(DocumentSummary summary) {
+ summaries.put(summary.getName(), summary);
+ }
+
+ /**
+ * <p>Returns a summary class defined by this search definition, or null if no summary with this name is defined.
+ * The default summary, named "default" is always present.</p>
+ *
+ * @param name the name of the summary to get.
+ * @return Summary found.
+ */
+ public DocumentSummary getSummary(String name) {
+ return summaries.get(name);
+ }
+
+ /**
+ * Returns the first explicit instance found of a summary field with this name, or null if not present (implicitly
+ * or explicitly) in any summary class.
+ *
+ * @param name The name of the summaryfield to get.
+ * @return SummaryField to return.
+ */
+ public SummaryField getSummaryField(String name) {
+ for (DocumentSummary summary : summaries.values()) {
+ SummaryField summaryField = summary.getSummaryField(name);
+ if (summaryField != null) {
+ return summaryField;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the first explicit instance found of a summary field with this name, or null if not present explicitly in
+ * any summary class
+ *
+ * @param name Thge name of the explicit summary field to get.
+ * @return The SummaryField found.
+ */
+ public SummaryField getExplicitSummaryField(String name) {
+ for (DocumentSummary summary : summaries.values()) {
+ SummaryField summaryField = summary.getSummaryField(name);
+ if (summaryField != null && !summaryField.isImplicit()) {
+ return summaryField;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Summaries defined by fields of this search definition. The default summary, named "default", is always the first
+ * one in the returned iterator.
+ *
+ * @return The map of document summaries.
+ */
+ public Map<String, DocumentSummary> getSummaries() {
+ return summaries;
+ }
+
+ /**
+ * <p>Returns all summary fields, of all document summaries, which has the given field as source. If there are
+ * multiple summary fields with the same name, the last one will be used (they should all have the same content, if
+ * this is a valid search definition).</p> <p>The map gets owned by the receiver.</p>
+ *
+ * @param field The source field.
+ * @return The map of summary fields found.
+ */
+ public Map<String, SummaryField> getSummaryFields(SDField field) {
+ Map<String, SummaryField> summaryFields = new java.util.LinkedHashMap<>();
+ for (DocumentSummary documentSummary : summaries.values()) {
+ for (SummaryField summaryField : documentSummary.getSummaryFields()) {
+ if (summaryField.hasSource(field.getName())) {
+ summaryFields.put(summaryField.getName(), summaryField);
+ }
+ }
+ }
+ return summaryFields;
+ }
+
+ /**
+ * <p>Returns one summary field for each summary field name. If there are multiple summary fields with the same
+ * name, the last one will be used. Multiple fields of the same name should all have the same content in a valid
+ * search definition, except from the destination set. So this method can be used for all summary handling except
+ * processing the destination set.</p> <p>The map gets owned by the receiver.</p>
+ *
+ * @return Map of unique summary fields
+ */
+ public Map<String, SummaryField> getUniqueNamedSummaryFields() {
+ Map<String, SummaryField> summaryFields = new java.util.LinkedHashMap<>();
+ for (DocumentSummary documentSummary : summaries.values()) {
+ for (SummaryField summaryField : documentSummary.getSummaryFields()) {
+ summaryFields.put(summaryField.getName(), summaryField);
+ }
+ }
+ return summaryFields;
+ }
+
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ /**
+ * Returns the first occurence of an attribute having this name, or null if none
+ *
+ * @param name Name of attribute
+ * @return The Attribute with given name.
+ */
+ public Attribute getAttribute(String name) {
+ for (SDField field : allFieldsList()) {
+ Attribute attribute = field.getAttributes().get(name);
+ if (attribute != null) {
+ return attribute;
+ }
+ }
+ return null;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Search)) {
+ return false;
+ }
+
+ Search other = (Search)o;
+ return getName().equals(other.getName());
+ }
+
+ public String toString() {
+ return "search definition '" + getName() + "'";
+ }
+
+ public boolean isAccessingDiskSummary(SummaryField field) {
+ if (!field.getTransform().isInMemory()) {
+ return true;
+ }
+ if (field.getSources().size() == 0) {
+ return isAccessingDiskSummary(getName());
+ }
+ for (SummaryField.Source source : field.getSources()) {
+ if (isAccessingDiskSummary(source.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isAccessingDiskSummary(String source) {
+ SDField field = getField(source);
+ if (field == null) {
+ return false;
+ }
+ if (field.doesSummarying() && !field.doesAttributing()) {
+ return true;
+ }
+ return false;
+ }
+
+ public void process() {
+ if (processed) {
+ throw new IllegalStateException("Search '" + getName() + "' already processed.");
+ }
+ processed = true;
+ }
+
+ public boolean isProcessed() {
+ return processed;
+ }
+
+ /**
+ * The field set settings for this search
+ *
+ * @return field set settings for this
+ */
+ public FieldSets fieldSets() {
+ return fieldSets;
+ }
+
+ /**
+ * For adding structs defined in document scope
+ *
+ * @param dt The struct to add.
+ * @return self, for chaining
+ */
+ public Search addType(SDDocumentType dt) {
+ docType.addType(dt); // TODO This is a very very dirty thing. It must go
+ return this;
+ }
+
+ public Search addAnnotation(SDAnnotationType dt) {
+ docType.addAnnotation(dt);
+ return this;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java b/config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java
new file mode 100644
index 00000000000..9c9bc559dc3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/SearchBuilder.java
@@ -0,0 +1,429 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.io.IOUtils;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.searchdefinition.derived.SearchOrderer;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.searchdefinition.parser.SDParser;
+import com.yahoo.searchdefinition.parser.SimpleCharStream;
+import com.yahoo.searchdefinition.parser.TokenMgrError;
+import com.yahoo.searchdefinition.processing.Processing;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+/**
+ * Helper class for importing {@link Search} objects in an unambiguous way. The pattern for using this is to 1) Import
+ * all available search definitions, using the importXXX() methods, 2) provide the available rank types and rank
+ * expressions, using the setRankXXX() methods, 3) invoke the {@link #build()} method, and 4) retrieve the built
+ * search objects using the {@link #getSearch(String)} method.
+ *
+ * @author TODO: Who created this?
+ */
+// TODO: This should be cleaned up and more or maybe completely taken over by MockApplicationPackage
+public class SearchBuilder {
+
+ private final DocumentTypeManager docTypeMgr = new DocumentTypeManager();
+ private List<Search> searchList = new LinkedList<>();
+ private ApplicationPackage app = null;
+ private boolean isBuilt = false;
+ private DocumentModel model = new DocumentModel();
+ private RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+
+ public SearchBuilder() {
+ this.app = MockApplicationPackage.createEmpty();
+ }
+
+ public SearchBuilder(ApplicationPackage app) {
+ this.app = app;
+ }
+
+ public SearchBuilder(ApplicationPackage app, RankProfileRegistry rankProfileRegistry) {
+ this.app = app;
+ this.rankProfileRegistry = rankProfileRegistry;
+ }
+
+ public SearchBuilder(RankProfileRegistry rankProfileRegistry) {
+ this(MockApplicationPackage.createEmpty(), rankProfileRegistry);
+ }
+
+ /**
+ * Import search definition.
+ *
+ * @param fileName The name of the file to import.
+ * @param deployLogger Logger for deploy messages.
+ * @return The name of the imported object.
+ * @throws IOException Thrown if the file can not be read for some reason.
+ * @throws ParseException Thrown if the file does not contain a valid search definition. ```
+ */
+ public String importFile(String fileName, DeployLogger deployLogger) throws IOException, ParseException {
+ File file = new File(fileName);
+ return importString(IOUtils.readFile(file), file.getAbsoluteFile().getParent(), deployLogger);
+ }
+
+ /**
+ * Import search definition.
+ *
+ * @param fileName The name of the file to import.
+ * @return The name of the imported object.
+ * @throws IOException Thrown if the file can not be read for some reason.
+ * @throws ParseException Thrown if the file does not contain a valid search definition.
+ */
+ public String importFile(String fileName) throws IOException, ParseException {
+ return importFile(fileName, new BaseDeployLogger());
+ }
+ public String importFile(Path file) throws IOException, ParseException {
+ return importFile(file.toString(), new BaseDeployLogger());
+ }
+
+ /**
+ * Reads and parses the search definition string provided by the given reader. Once all search definitions have been
+ * imported, call {@link #build()}.
+ *
+ * @param reader The reader whose content to import.
+ * @param searchDefDir The path to use when resolving file references.
+ * @return The name of the imported object.
+ * @throws ParseException Thrown if the file does not contain a valid search definition.
+ */
+ public String importReader(NamedReader reader, String searchDefDir, DeployLogger deployLogger) throws IOException, ParseException {
+ return importString(IOUtils.readAll(reader), searchDefDir, deployLogger);
+ }
+
+ /**
+ * See #{@link #importReader}
+ *
+ * Convenience, should only be used for testing as logs will be swallowed.
+ */
+ public String importReader(NamedReader reader, String searchDefDir) throws IOException, ParseException {
+ return importString(IOUtils.readAll(reader), searchDefDir, new BaseDeployLogger());
+ }
+
+ /**
+ * Import search definition.
+ *
+ * @param str the string to parse.
+ * @return the name of the imported object.
+ * @throws ParseException thrown if the file does not contain a valid search definition.
+ */
+ public String importString(String str) throws ParseException {
+ return importString(str, null, new BaseDeployLogger());
+ }
+
+ private String importString(String str, String searchDefDir, DeployLogger deployLogger) throws ParseException {
+ Search search;
+ SimpleCharStream stream = new SimpleCharStream(str);
+ try {
+ search = new SDParser(stream, deployLogger, app, rankProfileRegistry).search(docTypeMgr, searchDefDir);
+ } catch (TokenMgrError e) {
+ throw new ParseException("Unknown symbol: " + e.getMessage());
+ } catch (ParseException pe) {
+ throw new ParseException(stream.formatException(pe.getMessage()));
+ }
+ return importRawSearch(search);
+ }
+
+ /**
+ * Registers the given search object to the internal list of objects to be processed during {@link #build()}. A
+ * {@link Search} object is considered to be "raw" if it has not already been processed. This is the case for most
+ * programmatically constructed search objects used in unit tests.
+ *
+ * @param rawSearch The object to import.
+ * @return The name of the imported object.
+ * @throws IllegalArgumentException Thrown if the given search object has already been processed.
+ */
+ public String importRawSearch(Search rawSearch) {
+ if (rawSearch.getName() == null) {
+ throw new IllegalArgumentException("Search has no name.");
+ }
+ String rawName = rawSearch.getName();
+ if (rawSearch.isProcessed()) {
+ throw new IllegalArgumentException("A search definition with a search section called '" + rawName +
+ "' has already been processed.");
+ }
+ for (Search search : searchList) {
+ if (rawName.equals(search.getName())) {
+ throw new IllegalArgumentException("A search definition with a search section called '" + rawName +
+ "' has already been added.");
+ }
+ }
+ searchList.add(rawSearch);
+ return rawName;
+ }
+
+ /**
+ * Registers the given search object to the internal list of objects to be processed during {@link #build()}. A
+ * {@link Search} object is considered to be "processed" if it has not already been processed. This is the case for most
+ * programmatically constructed search objects used in unit tests.
+ *
+ * @param processed The object to import.
+ * @return The name of the imported object.
+ * @throws IllegalArgumentException Thrown if the given search object has already been processed.
+ */
+ public String importProcessedSearch(Search processed) {
+ if (processed.getName() == null) {
+ throw new IllegalArgumentException("Search has no name.");
+ }
+ String rawName = processed.getName();
+ if (!processed.isProcessed()) {
+ throw new IllegalArgumentException("A search definition with a search section called '" + rawName +
+ "' has not been processed.");
+ }
+ for (Search search : searchList) {
+ if (rawName.equals(search.getName())) {
+ throw new IllegalArgumentException("A search definition with a search section called '" + rawName +
+ "' has already been added.");
+ }
+ }
+ searchList.add(processed);
+ return rawName;
+ }
+
+ /**
+ * Only for testing.
+ *
+ * Processes and finalizes the imported search definitions so that they become available through the {@link
+ * #getSearch(String)} method.
+ *
+ * @throws IllegalStateException Thrown if this method has already been called.
+ */
+ public void build() {
+ build(new BaseDeployLogger(), new QueryProfiles());
+ }
+
+ /**
+ * Processes and finalizes the imported search definitions so that they become available through the {@link
+ * #getSearch(String)} method.
+ *
+ * @throws IllegalStateException Thrown if this method has already been called.
+ * @param deployLogger The logger to use during build
+ * @param queryProfiles The query profiles contained in the application this search is part of.
+ */
+ public void build(DeployLogger deployLogger, QueryProfiles queryProfiles) {
+ if (isBuilt) {
+ throw new IllegalStateException("Searches already built.");
+ }
+ List<Search> built = new ArrayList<>();
+ List<SDDocumentType> sdocs = new ArrayList<>();
+ sdocs.add(SDDocumentType.VESPA_DOCUMENT);
+ for (Search search : searchList) {
+ if (search.hasDocument()) {
+ sdocs.add(search.getDocument());
+ }
+ }
+ SDDocumentTypeOrderer orderer = new SDDocumentTypeOrderer(sdocs, deployLogger);
+ orderer.process();
+ for (SDDocumentType sdoc : orderer.getOrdered()) {
+ new FieldOperationApplierForStructs().process(sdoc);
+ new FieldOperationApplier().process(sdoc);
+ }
+
+ DocumentModelBuilder builder = new DocumentModelBuilder(model);
+ for (Search search : new SearchOrderer().order(searchList)) {
+ new FieldOperationApplierForSearch().process(search);
+ // These two needed for a couple of old unit tests, ideally these are just read from app
+ process(search, deployLogger, queryProfiles);
+ built.add(search);
+ }
+ builder.addToModel(searchList);
+ if ( ! builder.valid() ) {
+ throw new IllegalArgumentException("Impossible to build a correct model.");
+ }
+ searchList = built;
+ isBuilt = true;
+ }
+
+ /**
+ * Processes and returns the given {@link Search} object. This method has been factored out of the {@link
+ * #build()} method so that subclasses can choose not to build anything.
+ *
+ * @param search The object to build.
+ */
+ protected void process(Search search, DeployLogger deployLogger, QueryProfiles queryProfiles) {
+ Processing.process(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ /**
+ * Convenience method to call {@link #getSearch(String)} when there is only a single {@link Search} object
+ * built. This method will never return null.
+ *
+ * @return The build object.
+ * @throws IllegalStateException Thrown if there is not exactly one search.
+ */
+ public Search getSearch() {
+ if ( ! isBuilt) throw new IllegalStateException("Searches not built.");
+ if (searchList.size() != 1)
+ throw new IllegalStateException("This call only works if we have 1 search definition. Search definitions: " + searchList);
+
+ return searchList.get(0);
+ }
+
+ public DocumentModel getModel() {
+ return model;
+ }
+
+ /**
+ * Returns the built {@link Search} object that has the given name. If the name is unknown, this method will simply
+ * return null.
+ *
+ * @param name the name of the search definition to return,
+ * or null to return the only one or throw an exception if there are multiple to choose from
+ * @return the built object, or null if none with this name
+ * @throws IllegalStateException if {@link #build()} has not been called.
+ */
+ public Search getSearch(String name) {
+ if ( ! isBuilt) throw new IllegalStateException("Searches not built.");
+ if (name == null) return getSearch();
+
+ for (Search search : searchList)
+ if (search.getName().equals(name)) return search;
+ return null;
+ }
+
+ /**
+ * Convenience method to return a list of all built {@link Search} objects.
+ *
+ * @return The list of built searches.
+ */
+ public List<Search> getSearchList() {
+ return new ArrayList<>(searchList);
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a string.
+ *
+ * @param sd The string to build from.
+ * @return The built {@link SearchBuilder} object.
+ * @throws ParseException Thrown if there was a problem parsing the string.
+ */
+ public static SearchBuilder createFromString(String sd) throws ParseException {
+ SearchBuilder builder = new SearchBuilder(MockApplicationPackage.createEmpty());
+ builder.importString(sd);
+ builder.build();
+ return builder;
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a file. Only for testing.
+ *
+ * @param fileName the file to build from
+ * @return the built {@link SearchBuilder} object
+ * @throws IOException if there was a problem reading the file.
+ * @throws ParseException if there was a problem parsing the file content.
+ */
+ public static SearchBuilder createFromFile(String fileName) throws IOException, ParseException {
+ return createFromFile(fileName, new BaseDeployLogger(), new RankProfileRegistry());
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a file.
+ *
+ * @param fileName the file to build from.
+ * @param deployLogger logger for deploy messages.
+ * @param rankProfileRegistry registry for rank profiles.
+ * @return the built {@link SearchBuilder} object.
+ * @throws IOException if there was a problem reading the file.
+ * @throws ParseException if there was a problem parsing the file content.
+ */
+ public static SearchBuilder createFromFile(String fileName, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry)
+ throws IOException, ParseException {
+ SearchBuilder builder = new SearchBuilder(MockApplicationPackage.createEmpty(), rankProfileRegistry);
+ builder.importFile(fileName);
+ builder.build(deployLogger, new QueryProfiles());
+ return builder;
+ }
+
+ public static SearchBuilder createFromDirectory(String dir) throws IOException, ParseException {
+ return createFromDirectory(dir, new RankProfileRegistry());
+ }
+ public static SearchBuilder createFromDirectory(String dir, RankProfileRegistry rankProfileRegistry) throws IOException, ParseException {
+ SearchBuilder builder = new SearchBuilder(MockApplicationPackage.fromSearchDefinitionDirectory(dir), rankProfileRegistry);
+ for (Iterator<Path> i = Files.list(new File(dir).toPath()).filter(p -> p.getFileName().toString().endsWith(".sd")).iterator(); i.hasNext(); ) {
+ builder.importFile(i.next());
+ }
+ builder.build(new BaseDeployLogger(), new QueryProfiles());
+ return builder;
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a file. Only for testing.
+ *
+ * @param fileName The file to build from.
+ * @return The built {@link Search} object.
+ * @throws IOException Thrown if there was a problem reading the file.
+ * @throws ParseException Thrown if there was a problem parsing the file content.
+ */
+ public static Search buildFromFile(String fileName) throws IOException, ParseException {
+ return buildFromFile(fileName, new BaseDeployLogger(), new RankProfileRegistry());
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a file.
+ *
+ * @param fileName The file to build from.
+ * @param rankProfileRegistry Registry for rank profiles.
+ * @return The built {@link Search} object.
+ * @throws IOException Thrown if there was a problem reading the file.
+ * @throws ParseException Thrown if there was a problem parsing the file content.
+ */
+ public static Search buildFromFile(String fileName, RankProfileRegistry rankProfileRegistry)
+ throws IOException, ParseException {
+ return buildFromFile(fileName, new BaseDeployLogger(), rankProfileRegistry);
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a file.
+ *
+ * @param fileName The file to build from.
+ * @param deployLogger Logger for deploy messages.
+ * @param rankProfileRegistry Registry for rank profiles.
+ * @return The built {@link Search} object.
+ * @throws IOException Thrown if there was a problem reading the file.
+ * @throws ParseException Thrown if there was a problem parsing the file content.
+ */
+ public static Search buildFromFile(String fileName, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry)
+ throws IOException, ParseException {
+ return createFromFile(fileName, deployLogger, rankProfileRegistry).getSearch();
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a raw object.
+ *
+ * @param rawSearch The raw object to build from.
+ * @return The built {@link SearchBuilder} object.
+ * @see #importRawSearch(Search)
+ */
+ public static SearchBuilder createFromRawSearch(Search rawSearch, RankProfileRegistry rankProfileRegistry) {
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importRawSearch(rawSearch);
+ builder.build();
+ return builder;
+ }
+
+ /**
+ * Convenience factory method to import and build a {@link Search} object from a raw object.
+ *
+ * @param rawSearch The raw object to build from.
+ * @return The built {@link Search} object.
+ * @see #importRawSearch(Search)
+ */
+ public static Search buildFromRawSearch(Search rawSearch, RankProfileRegistry rankProfileRegistry) {
+ return createFromRawSearch(rawSearch, rankProfileRegistry).getSearch();
+ }
+
+ public RankProfileRegistry getRankProfileRegistry() {
+ return rankProfileRegistry;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/UnprocessingSearchBuilder.java b/config-model/src/main/java/com/yahoo/searchdefinition/UnprocessingSearchBuilder.java
new file mode 100644
index 00000000000..3df0c1f9953
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/UnprocessingSearchBuilder.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.io.IOException;
+
+/**
+ * A SearchBuilder that does not run the processing chain for searches
+ *
+ */
+public class UnprocessingSearchBuilder extends SearchBuilder {
+
+ public UnprocessingSearchBuilder(ApplicationPackage app, RankProfileRegistry rankProfileRegistry) {
+ super(app, rankProfileRegistry);
+ }
+
+ public UnprocessingSearchBuilder() {
+ super();
+ }
+
+ public UnprocessingSearchBuilder(RankProfileRegistry rankProfileRegistry) {
+ super(rankProfileRegistry);
+ }
+
+ @Override
+ public void process(Search search, DeployLogger deployLogger, QueryProfiles queryProfiles) {
+ // empty
+ }
+
+ public static Search buildUnprocessedFromFile(String fileName) throws IOException, ParseException {
+ SearchBuilder builder = new UnprocessingSearchBuilder();
+ builder.importFile(fileName);
+ builder.build();
+ return builder.getSearch();
+ }
+} \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/UnproperSearch.java b/config-model/src/main/java/com/yahoo/searchdefinition/UnproperSearch.java
new file mode 100644
index 00000000000..beab254bd89
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/UnproperSearch.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.document.SDDocumentType;
+
+/**
+ * A search that was derived from an sd file containing no search element(s), only
+ * document specifications.
+ *
+ * @author vegardh
+ *
+ */
+ // Award for best class name goes to ...
+public class UnproperSearch extends Search {
+ // This class exists because the parser accepts SD files without search { ... , and
+ // there are unit tests using it too, BUT there are many nullpointer bugs if you try to
+ // deploy such a file. Using this class to try to catch those.
+ // TODO: Throw away this when we properly support doc-only SD files.
+
+ public UnproperSearch() {
+ // empty
+ }
+
+ @Override
+ public void addDocument(SDDocumentType docType) {
+ if (getName() == null) {
+ setName(docType.getName());
+ }
+ super.addDocument(docType);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/UnrankedRankProfile.java b/config-model/src/main/java/com/yahoo/searchdefinition/UnrankedRankProfile.java
new file mode 100644
index 00000000000..f465112fbaf
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/UnrankedRankProfile.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+
+import com.yahoo.searchlib.rankingexpression.RankingExpression;
+import com.yahoo.searchlib.rankingexpression.parser.ParseException;
+
+/**
+ * A low-cost ranking profile to use for watcher queries etc.
+ *
+ * @author <a href="mailto:vegardh@yahoo-inc.com">Vegard Havdal</a>
+ */
+public class UnrankedRankProfile extends RankProfile {
+
+ public UnrankedRankProfile(Search search, RankProfileRegistry rankProfileRegistry) {
+ super("unranked", search, rankProfileRegistry);
+ try {
+ RankingExpression exp = new RankingExpression("value(0)");
+ this.setFirstPhaseRanking(exp);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Could not parse the ranking expression 'value(0)' when setting up " +
+ "the 'unranked' rank profile");
+ }
+ this.setIgnoreDefaultRankFeatures(true);
+ this.setKeepRankCount(0);
+ this.setRerankCount(0);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/AttributeFields.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/AttributeFields.java
new file mode 100644
index 00000000000..1130e5630a3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/AttributeFields.java
@@ -0,0 +1,157 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.Ranking;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Sorting;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.config.subscription.ConfigInstanceUtil;
+import com.yahoo.vespa.indexinglanguage.expressions.ToPositionExpression;
+
+import java.util.*;
+
+/**
+ * The set of all attribute fields defined by a search definition
+ *
+ * @author <a href="mailto:bratseth@overture.com">Jon S Bratseth</a>
+ */
+public class AttributeFields extends Derived implements AttributesConfig.Producer {
+ private Map<String, Attribute> attributes = new java.util.LinkedHashMap<>();
+
+ /**
+ * Flag indicating if a position-attribute has been found
+ */
+ private boolean hasPosition = false;
+
+ public AttributeFields(Search search) {
+ derive(search);
+ }
+
+ /**
+ * Derives everything from a field
+ */
+ protected void derive(SDField field, Search search) {
+ if (field.usesStructOrMap() &&
+ !field.getDataType().equals(PositionDataType.INSTANCE) &&
+ !field.getDataType().equals(DataType.getArray(PositionDataType.INSTANCE)))
+ {
+ return; // Ignore struct fields for indexed search (only implemented for streaming search)
+ }
+ deriveAttributes(field);
+ }
+
+ /**
+ * Return an attribute by name, or null if it doesn't exist
+ */
+ public Attribute getAttribute(String attributeName) {
+ return attributes.get(attributeName);
+ }
+
+ public boolean containsAttribute(String attributeName) {
+ return getAttribute(attributeName) != null;
+ }
+
+ /**
+ * Derives one attribute. TODO: Support non-default named attributes
+ */
+ private void deriveAttributes(SDField field) {
+ for (Attribute fieldAttribute : field.getAttributes().values()) {
+ Attribute attribute = getAttribute(fieldAttribute.getName());
+ if (attribute == null) {
+ attributes.put(fieldAttribute.getName(), fieldAttribute);
+ attribute = getAttribute(fieldAttribute.getName());
+ }
+ Ranking ranking = field.getRanking();
+ if (ranking != null && ranking.isFilter()) {
+ attribute.setEnableBitVectors(true);
+ attribute.setEnableOnlyBitVector(true);
+ }
+ }
+
+ if (field.containsExpression(ToPositionExpression.class)) {
+ // TODO: Move this check to processing and remove this
+ if (hasPosition) {
+ throw new IllegalArgumentException("Can not specify more than one " +
+ "set of position attributes per " + "field: " + field.getName());
+ }
+ hasPosition = true;
+ }
+ }
+
+ /**
+ * Returns a read only attribute iterator
+ */
+ public Iterator attributeIterator() {
+ return attributes().iterator();
+ }
+
+ public Collection<Attribute> attributes() {
+ return Collections.unmodifiableCollection(attributes.values());
+ }
+
+ public String toString() {
+ return "attributes " + getName();
+ }
+
+ protected String getDerivedName() {
+ return "attributes";
+ }
+
+ private Map<String, AttributesConfig.Attribute.Builder> toMap(List<AttributesConfig.Attribute.Builder> ls) {
+ Map<String, AttributesConfig.Attribute.Builder> ret = new LinkedHashMap<>();
+ for (AttributesConfig.Attribute.Builder builder : ls) {
+ ret.put((String) ConfigInstanceUtil.getField(builder, "name"), builder);
+ }
+ return ret;
+ }
+
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ for (Attribute attribute : attributes.values()) {
+ AttributesConfig.Attribute.Builder aaB = new AttributesConfig.Attribute.Builder()
+ .name(attribute.getName())
+ .datatype(AttributesConfig.Attribute.Datatype.Enum.valueOf(attribute.getType().getExportAttributeTypeName()))
+ .collectiontype(AttributesConfig.Attribute.Collectiontype.Enum.valueOf(attribute.getCollectionType().getName()));
+ if (attribute.isRemoveIfZero()) {
+ aaB.removeifzero(true);
+ }
+ if (attribute.isCreateIfNonExistent()) {
+ aaB.createifnonexistent(true);
+ }
+ aaB.enablebitvectors(attribute.isEnabledBitVectors());
+ aaB.enableonlybitvector(attribute.isEnabledOnlyBitVector());
+ if (attribute.isFastSearch()) {
+ aaB.fastsearch(true);
+ }
+ if (attribute.isFastAccess()) {
+ aaB.fastaccess(true);
+ }
+ if (attribute.isHuge()) {
+ aaB.huge(true);
+ }
+ if (attribute.getSorting().isDescending()) {
+ aaB.sortascending(false);
+ }
+ if (attribute.getSorting().getFunction() != Sorting.Function.UCA) {
+ aaB.sortfunction(AttributesConfig.Attribute.Sortfunction.Enum.valueOf(attribute.getSorting().getFunction().toString()));
+ }
+ if (attribute.getSorting().getStrength() != Sorting.Strength.PRIMARY) {
+ aaB.sortstrength(AttributesConfig.Attribute.Sortstrength.Enum.valueOf(attribute.getSorting().getStrength().toString()));
+ }
+ if (!attribute.getSorting().getLocale().isEmpty()) {
+ aaB.sortlocale(attribute.getSorting().getLocale());
+ }
+ aaB.arity(attribute.arity());
+ aaB.lowerbound(attribute.lowerBound());
+ aaB.upperbound(attribute.upperBound());
+ aaB.densepostinglistthreshold(attribute.densePostingListThreshold());
+ if (attribute.tensorType().isPresent()) {
+ aaB.tensortype(attribute.tensorType().get().toString());
+ }
+ builder.attribute(aaB);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/Derived.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Derived.java
new file mode 100644
index 00000000000..643eeb23c87
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Derived.java
@@ -0,0 +1,136 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigInstance.Builder;
+import com.yahoo.io.IOUtils;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.text.StringUtilities;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Superclass of all derived configurations
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public abstract class Derived implements Exportable {
+
+ private String name;
+
+ public String getName() { return name; }
+
+ protected final void setName(String name) { this.name=name; }
+
+ /**
+ * Derives the content of this configuration. This
+ * default calls derive(Document) for each document
+ * and derive(SDField) for each search definition level field
+ * AND sets the name of this to the name of the input search definition
+ */
+ protected void derive(Search search) {
+ setName(search.getName());
+ derive(search.getDocument(), search);
+ for (Index index : search.getExplicitIndices()) {
+ derive(index, search);
+ }
+ for (SDField field : search.allExtraFields() ) {
+ derive(field,search);
+ }
+ }
+
+
+ /**
+ * Derives the content of this configuration. This
+ * default calls derive(SDField) for each document field
+ */
+ protected void derive(SDDocumentType document,Search search) {
+ for (Field field : document.fieldSet()) {
+ SDField sdField = (SDField) field;
+ if (!sdField.isExtraField()) {
+ derive(sdField, search);
+ }
+ }
+ }
+
+ /**
+ * Derives the content of this configuration. This
+ * default does nothing.
+ */
+ protected void derive(SDField field,Search search) {}
+
+ /**
+ * Derives the content of this configuration. This
+ * default does nothing.
+ */
+ protected void derive(Index index, Search search) {
+ }
+
+ protected abstract String getDerivedName();
+
+ /** Returns the value of getName if true, the given number as a string otherwise */
+ protected String getIndex(int number,boolean labels) {
+ return labels ? getName() : String.valueOf(number);
+ }
+
+ /**
+ * Exports this derived configuration to its .cfg file
+ * in toDirectory
+ *
+ * @param toDirectory the directory to export to, or null
+ *
+ */
+ public final void export(String toDirectory)
+ throws IOException {
+ Writer writer=null;
+ try {
+ String fileName=getDerivedName() + ".cfg";
+ if (toDirectory!=null) writer=IOUtils.createWriter(toDirectory + "/" + fileName,false);
+ try {
+ exportBuilderConfig(writer);
+ } catch (ClassNotFoundException | InstantiationException
+ | IllegalAccessException | NoSuchMethodException
+ | SecurityException | IllegalArgumentException
+ | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ finally {
+ if (writer!=null) IOUtils.closeWriter(writer);
+ }
+ }
+
+ /**
+ * Checks what this is a producer of, instantiate that and export to writer
+ */
+ // TODO move to ReflectionUtil, and move that to unexported pkg
+ private void exportBuilderConfig(Writer writer) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, IOException {
+ for (Class<?> intf : getClass().getInterfaces()) {
+ if (ConfigInstance.Producer.class.isAssignableFrom(intf)) {
+ Class<?> configClass = intf.getEnclosingClass();
+ String builderClassName = configClass.getCanonicalName()+"$Builder";
+ Class<?> builderClass = Class.forName(builderClassName);
+ ConfigInstance.Builder builder = (Builder) builderClass.newInstance();
+ Method getConfig = getClass().getMethod("getConfig", builderClass);
+ getConfig.invoke(this, builder);
+ ConfigInstance inst = (ConfigInstance) configClass.getConstructor(builderClass).newInstance(builder);
+ List<String> payloadL = ConfigInstance.serialize(inst);
+ String payload = StringUtilities.implodeMultiline(payloadL);
+ writer.write(payload);
+ }
+ }
+ }
+
+ @Override
+ public String getFileName() {
+ return getDerivedName() + ".cfg";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java
new file mode 100644
index 00000000000..973e4e6100c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/DerivedConfiguration.java
@@ -0,0 +1,184 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.document.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.io.IOUtils;
+import com.yahoo.protect.Validator;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.derived.validation.Validation;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.List;
+
+/**
+ * A set of all derived configuration of a search definition. Use this as a facade to individual configurations when
+ * necessary.
+ *
+ * @author bratseth
+ */
+public class DerivedConfiguration {
+
+ private Search search;
+ private Summaries summaries;
+ private SummaryMap summaryMap;
+ private Juniperrc juniperrc;
+ private AttributeFields attributeFields;
+ private RankProfileList rankProfileList;
+ private IndexingScript indexingScript;
+ private IndexInfo indexInfo;
+ private VsmFields streamingFields;
+ private VsmSummary streamingSummary;
+ private IndexSchema indexSchema;
+
+ /**
+ * Creates a complete derived configuration from a search definition.
+ *
+ * @param search The search to derive a configuration from. Derived objects will be snapshots, but this argument is
+ * live. Which means that this object will be inconsistent when the given search definition is later
+ * modified.
+ * @param rankProfileRegistry a {@link com.yahoo.searchdefinition.RankProfileRegistry}
+ */
+ public DerivedConfiguration(Search search, RankProfileRegistry rankProfileRegistry) {
+ this(search, null, new BaseDeployLogger(), rankProfileRegistry);
+ }
+
+ /**
+ * Creates a complete derived configuration snapshot from a search definition.
+ *
+ * @param search The search to derive a configuration from. Derived objects will be snapshots, but this
+ * argument is live. Which means that this object will be inconsistent when the given
+ * search definition is later modified.
+ * @param abstractSearchList Search definition this one inherits from, only superclass configuration should be
+ * generated. Null or empty list if there is none.
+ * @param deployLogger a {@link DeployLogger} for logging when
+ * doing operations on this
+ * @param rankProfileRegistry a {@link com.yahoo.searchdefinition.RankProfileRegistry}
+ */
+ public DerivedConfiguration(Search search, List<Search> abstractSearchList, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry) {
+ Validator.ensureNotNull("Search definition", search);
+ if (!search.isProcessed()) {
+ throw new IllegalArgumentException("Search '" + search.getName() + "' not processed.");
+ }
+ this.search = search;
+ if (!search.isDocumentsOnly()) {
+ streamingFields = new VsmFields(search);
+ streamingSummary = new VsmSummary(search);
+ }
+ if (abstractSearchList != null) {
+ for (Search abstractSearch : abstractSearchList) {
+ if (!abstractSearch.isProcessed()) {
+ throw new IllegalArgumentException("Search '" + search.getName() + "' not processed.");
+ }
+ }
+ }
+ if (!search.isDocumentsOnly()) {
+ summaries = new Summaries(search, deployLogger);
+ summaryMap = new SummaryMap(search, summaries);
+ juniperrc = new Juniperrc(search);
+ attributeFields = new AttributeFields(search);
+ rankProfileList = new RankProfileList(search, attributeFields, rankProfileRegistry);
+ indexingScript = new IndexingScript(search);
+ indexInfo = new IndexInfo(search);
+ indexSchema = new IndexSchema(search);
+ }
+ Validation.validate(this, search);
+ }
+
+ /**
+ * Exports a complete set of configuration-server format config files.
+ *
+ * @param toDirectory the directory to export to, current dir if null
+ * @throws IOException if exporting fails, some files may still be created
+ */
+ public void export(String toDirectory) throws IOException {
+ if (!search.isDocumentsOnly()) {
+ summaries.export(toDirectory);
+ summaryMap.export(toDirectory);
+ juniperrc.export(toDirectory);
+ attributeFields.export(toDirectory);
+ streamingFields.export(toDirectory);
+ streamingSummary.export(toDirectory);
+ indexSchema.export(toDirectory);
+ rankProfileList.export(toDirectory);
+ indexingScript.export(toDirectory);
+ indexInfo.export(toDirectory);
+ }
+ }
+
+ public static void exportDocuments(DocumentmanagerConfig.Builder documentManagerCfg, String toDirectory) throws IOException {
+ exportCfg(new DocumentmanagerConfig(documentManagerCfg), toDirectory + "/" + "documentmanager.cfg");
+ }
+
+ public static void exportDocuments(DocumenttypesConfig.Builder documentTypesCfg, String toDirectory) throws IOException {
+ exportCfg(new DocumenttypesConfig(documentTypesCfg), toDirectory + "/" + "documenttypes.cfg");
+ }
+
+ private static void exportCfg(ConfigInstance instance, String fileName) throws IOException {
+ Writer writer = null;
+ try {
+ writer = IOUtils.createWriter(fileName, false);
+ if (writer != null) {
+ writer.write(instance.toString());
+ writer.write("\n");
+ }
+ } finally {
+ if (writer != null) {
+ IOUtils.closeWriter(writer);
+ }
+ }
+ }
+
+ public Summaries getSummaries() {
+ return summaries;
+ }
+
+ public AttributeFields getAttributeFields() {
+ return attributeFields;
+ }
+
+ public IndexingScript getIndexingScript() {
+ return indexingScript;
+ }
+
+ public IndexInfo getIndexInfo() {
+ return indexInfo;
+ }
+
+ public void setIndexingScript(IndexingScript script) {
+ this.indexingScript = script;
+ }
+
+ public Search getSearch() {
+ return search;
+ }
+
+ public RankProfileList getRankProfileList() {
+ return rankProfileList;
+ }
+
+ public VsmSummary getVsmSummary() {
+ return streamingSummary;
+ }
+
+ public VsmFields getVsmFields() {
+ return streamingFields;
+ }
+
+ public IndexSchema getIndexSchema() {
+ return indexSchema;
+ }
+
+ public Juniperrc getJuniperrc() {
+ return juniperrc;
+ }
+
+ public SummaryMap getSummaryMap() {
+ return summaryMap;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/Deriver.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Deriver.java
new file mode 100644
index 00000000000..76bf4492788
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Deriver.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+import com.yahoo.document.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.UnprocessingSearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.configmodel.producers.DocumentManager;
+import com.yahoo.vespa.configmodel.producers.DocumentTypes;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Auxiliary facade for deriving configs from search definitions
+ *
+ * @author bratseth
+ */
+public class Deriver {
+
+ /**
+ * Derives only document manager.
+ *
+ *
+ * @param sdFileNames The name of the search definition files to derive from.
+ * @param toDir The directory to write configuration to.
+ * @return The list of Search objects, possibly "unproper ones", from sd files containing only document
+ */
+ public static SearchBuilder deriveDocuments(List<String> sdFileNames, String toDir) {
+ SearchBuilder builder = getUnprocessingSearchBuilder(sdFileNames);
+ DocumentmanagerConfig.Builder documentManagerCfg = new DocumentManager().produce(builder.getModel(), new DocumentmanagerConfig.Builder());
+ try {
+ DerivedConfiguration.exportDocuments(documentManagerCfg, toDir);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ return builder;
+ }
+
+ public static SearchBuilder getSearchBuilder(List<String> sds) {
+ SearchBuilder builder = new SearchBuilder();
+ try {
+ for (String s : sds) {
+ builder.importFile(s);
+ }
+ } catch (ParseException | IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ builder.build();
+ return builder;
+ }
+
+ public static SearchBuilder getUnprocessingSearchBuilder(List<String> sds) {
+ SearchBuilder builder = new UnprocessingSearchBuilder();
+ try {
+ for (String s : sds) {
+ builder.importFile(s);
+ }
+ } catch (ParseException | IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ builder.build();
+ return builder;
+ }
+
+ public static DocumentmanagerConfig.Builder getDocumentManagerConfig(String sd) {
+ return getDocumentManagerConfig(Collections.singletonList(sd));
+ }
+
+ public static DocumentmanagerConfig.Builder getDocumentManagerConfig(List<String> sds) {
+ return new DocumentManager().produce(getSearchBuilder(sds).getModel(), new DocumentmanagerConfig.Builder());
+ }
+
+ public static DocumenttypesConfig.Builder getDocumentTypesConfig(String sd) {
+ return getDocumentTypesConfig(Collections.singletonList(sd));
+ }
+
+ public static DocumenttypesConfig.Builder getDocumentTypesConfig(List<String> sds) {
+ return new DocumentTypes().produce(getSearchBuilder(sds).getModel(), new DocumenttypesConfig.Builder());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/Exportable.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Exportable.java
new file mode 100644
index 00000000000..bcc295b91e0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Exportable.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+/**
+ * Classes exportable to configurations
+ *
+ * @author <a href="mailto:bratseth@overture.com">bratseth</a>
+ */
+public interface Exportable {
+
+ /**
+ * Exports the configuration of this object
+ *
+ *
+ * @param toDirectory the directory to export to, does not write to disk if null
+ * @throws java.io.IOException if exporting fails, some files may still be created
+ */
+ public void export(String toDirectory) throws java.io.IOException;
+
+ /**
+ * The (short) name of the exported file
+ * @return a String with the (short) name of the exported file
+ */
+ public String getFileName();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldRankSettings.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldRankSettings.java
new file mode 100644
index 00000000000..9dfc1bc2af7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldRankSettings.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * The rank settings of a field used for native rank features.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class FieldRankSettings {
+
+ private static final Logger logger = Logger.getLogger(FieldRankSettings.class.getName());
+
+ private String fieldName;
+
+ private final Map<String, NativeTable> tables = new LinkedHashMap<>();
+
+ public FieldRankSettings(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ public void addTable(NativeTable table) {
+ NativeTable existing = tables.get(table.getType().getName());
+ if (existing != null) {
+ logger.info("Using already specified rank table " + existing + " for field " + fieldName + ", not " + table);
+ return;
+ }
+ tables.put(table.getType().getName(), table);
+ }
+
+ public static boolean isIndexFieldTable(NativeTable table) {
+ return isFieldMatchTable(table) || isProximityTable(table);
+ }
+
+ public static boolean isAttributeFieldTable(NativeTable table) {
+ return isAttributeMatchTable(table);
+ }
+
+ private static boolean isFieldMatchTable(NativeTable table) {
+ return (table.getType().equals(NativeTable.Type.FIRST_OCCURRENCE) ||
+ table.getType().equals(NativeTable.Type.OCCURRENCE_COUNT));
+ }
+
+ private static boolean isAttributeMatchTable(NativeTable table) {
+ return (table.getType().equals(NativeTable.Type.WEIGHT));
+ }
+
+ private static boolean isProximityTable(NativeTable table) {
+ return (table.getType().equals(NativeTable.Type.PROXIMITY) ||
+ table.getType().equals(NativeTable.Type.REVERSE_PROXIMITY));
+ }
+
+ public Map<String,String> deriveRankProperties(int part) {
+ Map<String,String> ret = new LinkedHashMap<>();
+ int i = part;
+ for (Iterator<NativeTable> itr = tables.values().iterator(); itr.hasNext(); ++i) {
+ NativeTable table = itr.next();
+ if (isFieldMatchTable(table)) {
+ ret.put("nativeFieldMatch." + table.getType().getName() + "." + fieldName + ".part" + i, table.getName());
+ }
+ if (isAttributeMatchTable(table)) {
+ ret.put("nativeAttributeMatch." + table.getType().getName() + "." + fieldName + ".part" + i, table.getName());
+ }
+ if (isProximityTable(table)) {
+ ret.put("nativeProximity." + table.getType().getName() + "." + fieldName + ".part" + i, table.getName());
+ }
+ }
+ return ret;
+ }
+
+ public String toString() {
+ return "rank settings of field " + fieldName;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldResultTransform.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldResultTransform.java
new file mode 100644
index 00000000000..d61e57a621e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/FieldResultTransform.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+
+/**
+ * The result transformation of a named field
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class FieldResultTransform {
+
+ private String fieldName;
+
+ private SummaryTransform transform;
+
+ private String argument;
+
+ public FieldResultTransform(String fieldName,SummaryTransform transform,String argument) {
+ this.fieldName=fieldName;
+ this.transform=transform;
+ this.argument = argument;
+ }
+
+ public String getFieldName() { return fieldName; }
+
+ public SummaryTransform getTransform() { return transform; }
+
+ public void setTransform(SummaryTransform transform) { this.transform=transform; }
+
+ /** Returns the argument of this (used as input to the backend docsum rewriter) */
+ public String getArgument() { return argument; }
+
+ public int hashCode() {
+ return fieldName.hashCode() + 11*transform.hashCode() + 17* argument.hashCode();
+ }
+
+ public boolean equals(Object o) {
+ if (! (o instanceof FieldResultTransform)) return false;
+ FieldResultTransform other=(FieldResultTransform)o;
+
+ return
+ this.fieldName.equals(other.fieldName) &&
+ this.transform.equals(other.transform) &&
+ this.argument.equals(other.argument);
+ }
+
+ public String toString() {
+ String sourceString="";
+ if ( ! argument.equals(fieldName))
+ sourceString=" (argument: " + argument + ")";
+ return "field " + fieldName + ": " + transform + sourceString;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/Index.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Index.java
new file mode 100644
index 00000000000..cab4176d696
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Index.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.document.NumericDataType;
+import com.yahoo.document.datatypes.*;
+
+/**
+ * A type of an index structure
+ *
+ * @author bratseth
+ */
+public class Index {
+
+ /** The index type enumeration */
+ public static class Type {
+
+ public static final Type TEXT=new Type("text");
+ public static final Type INT64=new Type("long");
+ public static final Type BOOLEANTREE=new Type("booleantree");
+
+ private String name;
+
+ private Type(String name) {
+ this.name=name;
+ }
+
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ public String getName() { return name; }
+
+ public boolean equals(Object other) {
+ if ( ! (other instanceof Type)) return false;
+ return this.name.equals(((Type)other).name);
+ }
+
+ public String toString() {
+ return "type: " + name;
+ }
+
+ }
+
+ /** Sets the right index type from a field type */
+ public static Type convertType(DataType fieldType) {
+ FieldValue fval = fieldType.createFieldValue();
+ if (fieldType instanceof NumericDataType) {
+ return Type.INT64;
+ } else if (fval instanceof StringFieldValue) {
+ return Type.TEXT;
+ } else if (fval instanceof Raw) {
+ return Type.BOOLEANTREE;
+ } else if (fval instanceof PredicateFieldValue) {
+ return Type.BOOLEANTREE;
+ } else if (fieldType instanceof CollectionDataType) {
+ return convertType(((CollectionDataType) fieldType).getNestedType());
+ } else {
+ throw new IllegalArgumentException("Don't know which index type to " +
+ "convert " + fieldType + " to");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexInfo.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexInfo.java
new file mode 100644
index 00000000000..e98ee662b3a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexInfo.java
@@ -0,0 +1,559 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.*;
+import com.yahoo.searchdefinition.processing.ExactMatch;
+import com.yahoo.searchdefinition.processing.NGramMatch;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.search.config.IndexInfoConfig;
+
+import java.util.*;
+
+/**
+ * Per-index commands which should be applied to queries prior to searching
+ *
+ * @author bratseth
+ */
+public class IndexInfo extends Derived implements IndexInfoConfig.Producer {
+
+ private static final String CMD_ATTRIBUTE = "attribute";
+ private static final String CMD_DEFAULT_POSITION = "default-position";
+ private static final String CMD_DYNTEASER = "dynteaser";
+ private static final String CMD_FULLURL = "fullurl";
+ private static final String CMD_HIGHLIGHT = "highlight";
+ private static final String CMD_INDEX = "index";
+ private static final String CMD_LOWERCASE = "lowercase";
+ private static final String CMD_MATCH_GROUP = "match-group ";
+ private static final String CMD_NORMALIZE = "normalize";
+ private static final String CMD_STEM = "stem";
+ private static final String CMD_URLHOST = "urlhost";
+ private static final String CMD_WORD = "word";
+ private static final String CMD_PLAIN_TOKENS = "plain-tokens";
+ private static final String CMD_MULTIVALUE = "multivalue";
+ private static final String CMD_FAST_SEARCH = "fast-search";
+ private static final String CMD_PREDICATE_BOUNDS = "predicate-bounds";
+ private static final String CMD_NUMERICAL = "numerical";
+ private Set<IndexCommand> commands = new java.util.LinkedHashSet<>();
+ private Map<String, String> aliases = new java.util.LinkedHashMap<>();
+ private Map<String, FieldSet> fieldSets;
+ private Search search;
+
+ public IndexInfo(Search search) {
+ this.fieldSets = search.fieldSets().userFieldSets();
+ addIndexCommand("sddocname", CMD_INDEX);
+ addIndexCommand("sddocname", CMD_WORD);
+ derive(search);
+ }
+
+ protected void derive(Search search) {
+ super.derive(search); // Derive per field
+ this.search = search;
+ // Populate fieldsets with actual field objects, bit late to do that here but
+ for (FieldSet fs : fieldSets.values()) {
+ for (String fieldName : fs.getFieldNames()) {
+ fs.fields().add(search.getField(fieldName));
+ }
+ }
+ // Must follow, because index settings overrides field settings
+ for (Index index : search.getExplicitIndices()) {
+ derive(index, search);
+ }
+
+ // Commands for summary fields
+ // TODO: Move to fieldinfo and implement differently. This is not right
+ for (SummaryField summaryField : search.getUniqueNamedSummaryFields().values()) {
+ if (summaryField.getTransform().isTeaser()) {
+ addIndexCommand(summaryField.getName(), CMD_DYNTEASER);
+ }
+ if (summaryField.getTransform().isBolded()) {
+ addIndexCommand(summaryField.getName(), CMD_HIGHLIGHT);
+ }
+ }
+ }
+
+ protected void derive(Index index, Search search) {
+ if (index.getMatchGroup().size() > 0) {
+ addIndexCommand(index.getName(), CMD_MATCH_GROUP + toSpaceSeparated(index.getMatchGroup()));
+ }
+ }
+
+ private String toSpaceSeparated(Collection c) {
+ StringBuffer b = new StringBuffer();
+ for (Iterator i = c.iterator(); i.hasNext();) {
+ b.append(i.next());
+ if (i.hasNext()) {
+ b.append(" ");
+ }
+ }
+ return b.toString();
+ }
+
+ protected void derive(SDField field, Search search) {
+ if (field.getDataType().equals(DataType.PREDICATE)) {
+ Index index = field.getIndex(field.getName());
+ if (index != null) {
+ BooleanIndexDefinition options = index.getBooleanIndexDefiniton();
+ if (options.hasLowerBound() || options.hasUpperBound()) {
+ addIndexCommand(field.getName(), CMD_PREDICATE_BOUNDS + " [" +
+ (options.hasLowerBound() ? Long.toString(options.getLowerBound()) : "") + ".." +
+ (options.hasUpperBound() ? Long.toString(options.getUpperBound()) : "") + "]");
+ }
+ }
+ }
+
+ // Field level aliases
+ for (Map.Entry<String, String> e : field.getAliasToName().entrySet()) {
+ String alias = e.getKey();
+ String name = e.getValue();
+ addIndexAlias(alias, name);
+ }
+ if (field.usesStructOrMap()) {
+ for (SDField structField : field.getStructFields()) {
+ derive(structField, search); // Recursion
+ }
+ }
+
+ if (field.getDataType().equals(PositionDataType.INSTANCE) ||
+ field.getDataType().equals(DataType.getArray(PositionDataType.INSTANCE)))
+ {
+ addIndexCommand(field.getName(), CMD_DEFAULT_POSITION);
+ }
+
+ addIndexCommand(field, CMD_INDEX); // List the indices
+
+ if (field.doesIndexing() || field.doesLowerCasing()) {
+ addIndexCommand(field, CMD_LOWERCASE);
+ }
+
+ if (field.getDataType().isMultivalue()) {
+ addIndexCommand(field, CMD_MULTIVALUE);
+ }
+
+ if (field.doesAttributing() && !field.doesIndexing()) {
+ addIndexCommand(field.getName(), CMD_ATTRIBUTE);
+ Attribute attribute = field.getAttributes().get(field.getName());
+ if (attribute != null && attribute.isFastSearch())
+ addIndexCommand(field.getName(), CMD_FAST_SEARCH);
+ } else if (field.doesIndexing()) {
+ if (stemSomehow(field, search)) {
+ addIndexCommand(field, stemCmd(field, search), new StemmingOverrider(this, search));
+ }
+ if (normalizeAccents(field)) {
+ addIndexCommand(field, CMD_NORMALIZE);
+ }
+ }
+
+ if (isUriField(field)) {
+ addUriIndexCommands(field);
+ }
+
+ if (field.getDataType() instanceof NumericDataType) {
+ addIndexCommand(field, CMD_NUMERICAL);
+ }
+
+ // Explicit commands
+ for (String command : field.getQueryCommands()) {
+ addIndexCommand(field, command);
+ }
+
+ }
+
+ static String stemCmd(SDField field, Search search) {
+ return CMD_STEM + ":" + field.getStemming(search).toStemMode();
+ }
+
+ private boolean stemSomehow(SDField field, Search search) {
+ if (field.getStemming(search).equals(Stemming.NONE)) return false;
+ return isTypeOrNested(field, DataType.STRING);
+ }
+
+ private boolean normalizeAccents(SDField field) {
+ return field.getNormalizing().doRemoveAccents() && isTypeOrNested(field, DataType.STRING);
+ }
+
+ private boolean isTypeOrNested(SDField field, DataType type) {
+ return field.getDataType().equals(type) || field.getDataType().equals(DataType.getArray(type)) ||
+ field.getDataType().equals(DataType.getWeightedSet(type));
+ }
+
+ private boolean isUriField(Field field) {
+ DataType fieldType = field.getDataType();
+ if (DataType.URI.equals(fieldType)) {
+ return true;
+ }
+ if (fieldType instanceof CollectionDataType &&
+ DataType.URI.equals(((CollectionDataType)fieldType).getNestedType()))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ private void addUriIndexCommands(SDField field) {
+ String fieldName = field.getName();
+ addIndexCommand(fieldName, CMD_FULLURL);
+ addIndexCommand(fieldName, CMD_LOWERCASE);
+ addIndexCommand(fieldName + "." + fieldName, CMD_FULLURL);
+ addIndexCommand(fieldName + "." + fieldName, CMD_LOWERCASE);
+ addIndexCommand(fieldName + ".path", CMD_FULLURL);
+ addIndexCommand(fieldName + ".path", CMD_LOWERCASE);
+ addIndexCommand(fieldName + ".query", CMD_FULLURL);
+ addIndexCommand(fieldName + ".query", CMD_LOWERCASE);
+ addIndexCommand(fieldName + ".hostname", CMD_URLHOST);
+ addIndexCommand(fieldName + ".hostname", CMD_LOWERCASE);
+
+ // XXX hack
+ Index index = field.getIndex("hostname");
+ if (index != null) {
+ addIndexCommand(index, CMD_URLHOST);
+ }
+ }
+
+ /**
+ * Sets a command for all indices of a field
+ */
+ private void addIndexCommand(Index index, String command) {
+ addIndexCommand(index.getName(), command);
+ }
+
+ /**
+ * Sets a command for all indices of a field
+ */
+ private void addIndexCommand(SDField field, String command) {
+ addIndexCommand(field, command, null);
+ }
+
+ /**
+ * Sets a command for all indices of a field
+ */
+ private void addIndexCommand(SDField field, String command, IndexOverrider overrider) {
+ if (overrider == null || !overrider.override(field.getName(), command, field)) {
+ addIndexCommand(field.getName(), command);
+ }
+ }
+
+ private void addIndexCommand(String indexName, String command) {
+ commands.add(new IndexCommand(indexName, command));
+ }
+
+ private void addIndexAlias(String alias, String indexName) {
+ aliases.put(alias, indexName);
+ }
+
+ /**
+ * Returns whether a particular command is prsent in this index info
+ */
+ public boolean hasCommand(String indexName, String command) {
+ return commands.contains(new IndexCommand(indexName, command));
+ }
+
+ private boolean notInCommands(String index) {
+ for (IndexCommand command : commands) {
+ if (command.getIndex().equals(index)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ IndexInfoConfig.Indexinfo.Builder iiB = new IndexInfoConfig.Indexinfo.Builder();
+ iiB.name(getName());
+ for (IndexCommand command : commands) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(command.getIndex())
+ .command(command.getCommand()));
+ }
+ // Make user defined field sets searchable
+ for (FieldSet fieldSet : fieldSets.values()) {
+ if (notInCommands(fieldSet.getName())) {
+ addFieldSetCommands(iiB, fieldSet);
+ }
+ }
+
+ for (Map.Entry<String, String> e : aliases.entrySet()) {
+ iiB.alias(
+ new IndexInfoConfig.Indexinfo.Alias.Builder()
+ .alias(e.getKey())
+ .indexname(e.getValue()));
+ }
+ builder.indexinfo(iiB);
+ }
+
+ // TODO: This implementation is completely brain dead.
+ // Move it to the FieldSetValidity processor (and rename it) as that already has to look at this.
+ // Also add more explicit testing of this, e.g to indexinfo_fieldsets in ExportingTestCase. - Jon
+ private void addFieldSetCommands(IndexInfoConfig.Indexinfo.Builder iiB, FieldSet fieldSet) {
+ // Explicit query commands on the field set, overrides everything.
+ if (!fieldSet.queryCommands().isEmpty()) {
+ for (String qc : fieldSet.queryCommands()) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(qc));
+ }
+ return;
+ }
+ boolean anyIndexing = false;
+ boolean anyAttributing = false;
+ boolean anyLowerCasing = false;
+ boolean anyStemming = false;
+ boolean anyNormalizing = false;
+ String stemmingCommand = null;
+ Matching fieldSetMatching = fieldSet.getMatching(); // null if no explicit matching
+ // First a pass over the fields to read some params to decide field settings implicitly:
+ for (SDField field : fieldSet.fields()) {
+ if (field.doesIndexing()) {
+ anyIndexing = true;
+ }
+ if (field.doesAttributing()) {
+ anyAttributing = true;
+ }
+ if (field.doesIndexing() || field.doesLowerCasing()) {
+ anyLowerCasing = true;
+ }
+ if (stemming(field)) {
+ anyStemming = true;
+ stemmingCommand = CMD_STEM + ":" + getEffectiveStemming(field).toStemMode();
+ }
+ if (field.getNormalizing().doRemoveAccents()) {
+ anyNormalizing = true;
+ }
+ if (fieldSetMatching == null && field.getMatching().getType() != Matching.defaultType)
+ fieldSetMatching = field.getMatching();
+ }
+ if (anyIndexing && anyAttributing && fieldSet.getMatching() == null) {
+ // We have both attributes and indexes and no explicit match setting ->
+ // use default matching as that at least works if the data in the attribute consists
+ // of single tokens only.
+ fieldSetMatching = new Matching();
+ }
+ if (anyLowerCasing) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(CMD_LOWERCASE));
+ }
+ if (hasMultiValueField(fieldSet)) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(CMD_MULTIVALUE));
+ }
+ if (anyIndexing) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(CMD_INDEX));
+ if ( ! isExactMatch(fieldSetMatching)) {
+ if (anyStemming) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(stemmingCommand));
+ }
+ if (anyNormalizing)
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(CMD_NORMALIZE));
+ }
+ } else {
+ // Assume only attribute fields
+ iiB
+ .command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(CMD_ATTRIBUTE))
+ .command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(CMD_INDEX));
+ }
+ if (fieldSetMatching != null) {
+ // Explicit matching set on fieldset
+ if (fieldSetMatching.getType().equals(Matching.Type.EXACT)) {
+ String term = fieldSetMatching.getExactMatchTerminator();
+ if (term==null) term=ExactMatch.DEFAULT_EXACT_TERMINATOR;
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command("exact "+term));
+ } else if (fieldSetMatching.getType().equals(Matching.Type.WORD)) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command(CMD_WORD));
+ } else if (fieldSetMatching.getType().equals(Matching.Type.GRAM)) {
+ iiB.command(
+ new IndexInfoConfig.Indexinfo.Command.Builder()
+ .indexname(fieldSet.getName())
+ .command("ngram "+(fieldSetMatching.getGramSize()>0 ? fieldSetMatching.getGramSize() : NGramMatch.DEFAULT_GRAM_SIZE)));
+ } else if (fieldSetMatching.getType().equals(Matching.Type.TEXT)) {
+
+ }
+
+ }
+
+ }
+
+ private boolean hasMultiValueField(FieldSet fieldSet) {
+ for (SDField field : fieldSet.fields()) {
+ if (field.getDataType().isMultivalue())
+ return true;
+ }
+ return false;
+ }
+
+ private Stemming getEffectiveStemming(SDField field) {
+ Stemming active = field.getStemming(search);
+ if (field.getIndex(field.getName()) != null) {
+ if (field.getIndex(field.getName()).getStemming()!=null) {
+ active = field.getIndex(field.getName()).getStemming();
+ }
+ }
+ if (active != null) {
+ return active;
+ }
+ // assume default
+ return Stemming.SHORTEST;
+ }
+
+ private boolean stemming(SDField field) {
+ if (field.getStemming() != null) {
+ return !field.getStemming().equals(Stemming.NONE);
+ }
+ if (search.getStemming()==Stemming.NONE) return false;
+ if (field.getIndex(field.getName())==null) return true;
+ if (field.getIndex(field.getName()).getStemming()==null) return true;
+ return !(field.getIndex(field.getName()).getStemming().equals(Stemming.NONE));
+ }
+
+ private boolean isExactMatch(Matching m) {
+ if (m==null) return false;
+ if (m.getType().equals(Matching.Type.EXACT)) return true;
+ if (m.getType().equals(Matching.Type.WORD)) return true;
+ return false;
+ }
+
+ /**
+ * Returns a read only iterator over the index commands of this
+ */
+ public Iterator<IndexCommand> indexCommandIterator() {
+ return Collections.unmodifiableSet(commands).iterator();
+ }
+
+ protected String getDerivedName() {
+ return "index-info";
+ }
+
+ /**
+ * An index command. Null commands are also represented, to detect consistency issues. This is an (immutable) value
+ * object.
+ */
+ public static class IndexCommand {
+
+ private String index;
+
+ private String command;
+
+ public IndexCommand(String index, String command) {
+ this.index = index;
+ this.command = command;
+ }
+
+ public String getIndex() {
+ return index;
+ }
+
+ public String getCommand() {
+ return command;
+ }
+
+ /**
+ * Returns true if this is the null command (do nothing)
+ */
+ public boolean isNull() {
+ return command.equals("");
+ }
+
+ public int hashCode() {
+ return index.hashCode() + 17 * command.hashCode();
+ }
+
+ public boolean equals(Object object) {
+ if (!(object instanceof IndexCommand)) {
+ return false;
+ }
+
+ IndexCommand other = (IndexCommand)object;
+ return
+ other.index.equals(this.index) &&
+ other.command.equals(this.command);
+ }
+
+ public String toString() {
+ return "index command " + command + " on index " + index;
+ }
+
+ }
+
+ /**
+ * A command which may override the command setting of a field for a particular index
+ */
+ private static abstract class IndexOverrider {
+
+ protected IndexInfo owner;
+
+ public IndexOverrider(IndexInfo owner) {
+ this.owner = owner;
+ }
+
+ /**
+ * Override the setting of this index for this field, returns true if overriden, false if this index should be
+ * set according to the field
+ */
+ public abstract boolean override(String indexName, String command, SDField field);
+
+ }
+
+ private static class StemmingOverrider extends IndexOverrider {
+
+ private Search search;
+
+ public StemmingOverrider(IndexInfo owner, Search search) {
+ super(owner);
+ this.search = search;
+ }
+
+ public boolean override(String indexName, String command, SDField field) {
+ if (search == null) {
+ return false;
+ }
+
+ Index index = search.getIndex(indexName);
+ if (index == null) {
+ return false;
+ }
+
+ Stemming indexStemming = index.getStemming();
+ if (indexStemming == null) {
+ return false;
+ }
+
+ if (Stemming.NONE.equals(indexStemming)) {
+ // Add nothing
+ } else {
+ owner.addIndexCommand(indexName, CMD_STEM + ":" + indexStemming.toStemMode());
+ }
+ return true;
+ }
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java
new file mode 100644
index 00000000000..05b8a1bf5e7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexSchema.java
@@ -0,0 +1,231 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.BooleanIndexDefinition;
+import com.yahoo.searchdefinition.document.FieldSet;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.config.search.IndexschemaConfig;
+
+import java.util.*;
+
+/**
+ * Deriver of indexschema config containing information of all index fields with name and data type.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class IndexSchema extends Derived implements IndexschemaConfig.Producer {
+
+ private final List<IndexField> fields = new ArrayList<>();
+ private final Map<String, FieldCollection> collections = new LinkedHashMap<>();
+ private final Map<String, FieldSet> fieldSets = new LinkedHashMap<>();
+
+ public IndexSchema(Search search) {
+ fieldSets.putAll(search.fieldSets().userFieldSets());
+ derive(search);
+ }
+
+ public boolean containsField(String fieldName) {
+ return fields.stream().anyMatch(field -> field.getName().equals(fieldName));
+ }
+
+ @Override
+ protected void derive(Search search) {
+ super.derive(search);
+ }
+
+ private void deriveIndexFields(SDField field, Search search) {
+ if (!field.doesIndexing() &&
+ !field.isIndexStructureField())
+ {
+ return;
+ }
+ List<Field> lst = flattenField(field);
+ if (lst.isEmpty()) {
+ return;
+ }
+ String fieldName = field.getName();
+ for (Field flatField : lst) {
+ deriveIndexFields(flatField, search);
+ }
+ if (lst.size() > 1) {
+ FieldSet fieldSet = new FieldSet(fieldName);
+ for (Field flatField : lst) {
+ fieldSet.addFieldName(flatField.getName());
+ }
+ fieldSets.put(fieldName, fieldSet);
+ }
+ }
+
+ private void deriveIndexFields(Field field, Search search) {
+ IndexField toAdd = new IndexField(field.getName(), Index.convertType(field.getDataType()), field.getDataType());
+ com.yahoo.searchdefinition.Index definedIndex = search.getIndex(field.getName());
+ if (definedIndex != null) {
+ toAdd.setIndexSettings(definedIndex);
+ }
+ fields.add(toAdd);
+ addFieldToCollection(field.getName(), field.getName()); // implicit
+ }
+
+ private FieldCollection getCollection(String collectionName) {
+ FieldCollection retval = collections.get(collectionName);
+ if (retval == null) {
+ collections.put(collectionName, new FieldCollection(collectionName));
+ return collections.get(collectionName);
+ }
+ return retval;
+ }
+
+ private void addFieldToCollection(String fieldName, String collectionName) {
+ FieldCollection collection = getCollection(collectionName);
+ collection.fields.add(fieldName);
+ }
+
+ @Override
+ protected void derive(SDField field, Search search) {
+ if (field.usesStructOrMap()) {
+ return; // unsupported
+ }
+ deriveIndexFields(field, search);
+ }
+
+ @Override
+ protected String getDerivedName() {
+ return "indexschema";
+ }
+
+ @Override
+ public void getConfig(IndexschemaConfig.Builder icB) {
+ for (int i = 0; i < fields.size(); ++i) {
+ IndexField f = fields.get(i);
+ IndexschemaConfig.Indexfield.Builder ifB = new IndexschemaConfig.Indexfield.Builder()
+ .name(f.getName())
+ .datatype(IndexschemaConfig.Indexfield.Datatype.Enum.valueOf(f.getType()))
+ .prefix(f.hasPrefix())
+ .phrases(f.hasPhrases())
+ .positions(f.hasPositions());
+ if (f.getSdType() !=null && !f.getSdType().equals(com.yahoo.searchdefinition.Index.Type.VESPA)) {
+ ifB.indextype(IndexschemaConfig.Indexfield.Indextype.Enum.valueOf(f.getSdType().toString()));
+ }
+ if (!f.getCollectionType().equals("SINGLE")) {
+ ifB.collectiontype(IndexschemaConfig.Indexfield.Collectiontype.Enum.valueOf(f.getCollectionType()));
+ }
+ icB.indexfield(ifB);
+ }
+ for (FieldSet fieldSet : fieldSets.values()) {
+ IndexschemaConfig.Fieldset.Builder fsB = new IndexschemaConfig.Fieldset.Builder()
+ .name(fieldSet.getName());
+ for (String f : fieldSet.getFieldNames()) {
+ fsB.field(new IndexschemaConfig.Fieldset.Field.Builder()
+ .name(f));
+ }
+ icB.fieldset(fsB);
+ }
+ }
+
+ static List<Field> flattenField(Field field) {
+ DataType fieldType = field.getDataType();
+ if (fieldType.getPrimitiveType() != null){
+ return Collections.singletonList(field);
+ }
+ if (fieldType instanceof ArrayDataType) {
+ boolean header = field.isHeader();
+ List<Field> ret = new LinkedList<>();
+ Field innerField = new Field(field.getName(), ((ArrayDataType)fieldType).getNestedType(), header);
+ for (Field flatField : flattenField(innerField)) {
+ ret.add(new Field(flatField.getName(), DataType.getArray(flatField.getDataType()), header));
+ }
+ return ret;
+ }
+ if (fieldType instanceof StructuredDataType) {
+ List<Field> ret = new LinkedList<>();
+ String fieldName = field.getName();
+ for (Field childField : ((StructuredDataType)fieldType).getFields()) {
+ for (Field flatField : flattenField(childField)) {
+ ret.add(new Field(fieldName + "." + flatField.getName(), flatField));
+ }
+ }
+ return ret;
+ }
+ throw new UnsupportedOperationException(fieldType.getName());
+ }
+
+ public List<IndexField> getFields() {
+ return fields;
+ }
+
+ /**
+ * Representation of an index field with name and data type.
+ */
+ public static class IndexField {
+ private String name;
+ private Index.Type type;
+ private com.yahoo.searchdefinition.Index.Type sdType; // The index type in "user intent land"
+ private DataType sdFieldType;
+ private boolean prefix = false;
+ private boolean phrases = false; // TODO dead, but keep a while to ensure config compatibility?
+ private boolean positions = true;// TODO dead, but keep a while to ensure config compatibility?
+ private BooleanIndexDefinition boolIndex = null;
+
+ public IndexField(String name, Index.Type type, DataType sdFieldType) {
+ this.name = name;
+ this.type = type;
+ this.sdFieldType = sdFieldType;
+ }
+ public void setIndexSettings(com.yahoo.searchdefinition.Index index) {
+ if (type.equals(Index.Type.TEXT)) {
+ prefix = index.isPrefix();
+ }
+ sdType = index.getType();
+ boolIndex = index.getBooleanIndexDefiniton();
+ }
+ public String getName() { return name; }
+ public Index.Type getRawType() { return type; }
+ public String getType() {
+ return type.equals(Index.Type.INT64)
+ ? "INT64"
+ : type.equals(Index.Type.BOOLEANTREE)
+ ? "BOOLEANTREE"
+ : "STRING";
+ }
+ public String getCollectionType() {
+ return (sdFieldType == null)
+ ? "SINGLE"
+ : (sdFieldType instanceof WeightedSetDataType)
+ ? "WEIGHTEDSET"
+ : (sdFieldType instanceof ArrayDataType)
+ ? "ARRAY"
+ : "SINGLE";
+ }
+ public boolean hasPrefix() { return prefix; }
+ public boolean hasPhrases() { return phrases; }
+ public boolean hasPositions() { return positions; }
+
+ public BooleanIndexDefinition getBooleanIndexDefinition() {
+ return boolIndex;
+ }
+
+ /**
+ * The user set index type
+ * @return the type
+ */
+ public com.yahoo.searchdefinition.Index.Type getSdType() {
+ return sdType;
+ }
+ }
+
+ /**
+ * Representation of a collection of fields (aka index, physical view).
+ */
+ @SuppressWarnings({ "UnusedDeclaration" })
+ private static class FieldCollection {
+
+ private final String name;
+ private final List<String> fields = new ArrayList<>();
+
+ FieldCollection(String name) {
+ this.name = name;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexingScript.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexingScript.java
new file mode 100644
index 00000000000..94e4dec567f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/IndexingScript.java
@@ -0,0 +1,146 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.*;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.indexinglanguage.ExpressionVisitor;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig.Ilscript.Builder;
+
+import java.util.*;
+
+/**
+ * An indexing language script derived from a search definition. An indexing script contains a set of indexing
+ * statements, organized in a composite structure of indexing code snippets.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public final class IndexingScript extends Derived implements IlscriptsConfig.Producer {
+
+ private final List<String> docFields = new LinkedList<>();
+ private final List<Expression> expressions = new LinkedList<>();
+
+ public IndexingScript(Search search) {
+ derive(search);
+ }
+
+ protected void derive(SDField field, Search search) {
+ if (!field.isExtraField()) {
+ docFields.add(field.getName());
+ }
+ if (field.usesStructOrMap() &&
+ !field.getDataType().equals(PositionDataType.INSTANCE) &&
+ !field.getDataType().equals(DataType.getArray(PositionDataType.INSTANCE)))
+ {
+ return; // unsupported
+ }
+ ScriptExpression script = field.getIndexingScript();
+ if (!script.isEmpty()) {
+ expressions.add(new StatementExpression(new ClearStateExpression(),
+ new GuardExpression(script)));
+ }
+ }
+
+ public Iterable<Expression> expressions() {
+ return Collections.unmodifiableCollection(expressions);
+ }
+
+ @Override
+ public String getDerivedName() {
+ return "ilscripts";
+ }
+
+ @Override
+ public void getConfig(IlscriptsConfig.Builder configBuilder) {
+ IlscriptsConfig.Ilscript.Builder ilscriptBuilder = new IlscriptsConfig.Ilscript.Builder();
+ ilscriptBuilder.doctype(getName());
+ for (String fieldName : docFields) {
+ ilscriptBuilder.docfield(fieldName);
+ }
+ addContentInOrder(ilscriptBuilder);
+ configBuilder.ilscript(ilscriptBuilder);
+ }
+
+ private void addContentInOrder(IlscriptsConfig.Ilscript.Builder ilscriptBuilder) {
+ ArrayList<Expression> later = new ArrayList<>();
+ Set<String> touchedFields = new HashSet<String>();
+ for (Expression exp : expressions) {
+ FieldScanVisitor fieldFetcher = new FieldScanVisitor();
+ if (modifiesSelf(exp)) {
+ later.add(exp);
+ } else {
+ ilscriptBuilder.content(exp.toString());
+ }
+ fieldFetcher.visit(exp);
+ touchedFields.addAll(fieldFetcher.touchedFields());
+ }
+ for (Expression exp : later) {
+ ilscriptBuilder.content(exp.toString());
+ }
+ generateSyntheticStatementsForUntouchedFields(ilscriptBuilder, touchedFields);
+ }
+
+ private void generateSyntheticStatementsForUntouchedFields(Builder ilscriptBuilder, Set<String> touchedFields) {
+ Set<String> fieldsWithSyntheticStatements = new HashSet<String>(docFields);
+ fieldsWithSyntheticStatements.removeAll(touchedFields);
+ List<String> orderedFields = new ArrayList<String>(fieldsWithSyntheticStatements);
+ Collections.sort(orderedFields);
+ for (String fieldName : orderedFields) {
+ StatementExpression copyField = new StatementExpression(new InputExpression(fieldName),
+ new PassthroughExpression(fieldName));
+ ilscriptBuilder.content(copyField.toString());
+ }
+ }
+
+ private boolean modifiesSelf(Expression exp) {
+ MyExpVisitor visitor = new MyExpVisitor();
+ visitor.visit(exp);
+ return visitor.modifiesSelf();
+ }
+
+ private class MyExpVisitor extends ExpressionVisitor {
+ private String inputField = null;
+ private String outputField = null;
+
+ public boolean modifiesSelf() { return outputField != null && outputField.equals(inputField); }
+
+ @Override
+ protected void doVisit(Expression expression) {
+ if (modifiesSelf()) {
+ return;
+ }
+ if (expression instanceof InputExpression) {
+ inputField = ((InputExpression) expression).getFieldName();
+ }
+ if (expression instanceof OutputExpression) {
+ outputField = ((OutputExpression) expression).getFieldName();
+ }
+ }
+ }
+
+ private static class FieldScanVisitor extends ExpressionVisitor {
+ List<String> touchedFields = new ArrayList<String>();
+ List<String> candidates = new ArrayList<String>();
+
+ @Override
+ protected void doVisit(Expression exp) {
+ if (exp instanceof OutputExpression) {
+ touchedFields.add(((OutputExpression) exp).getFieldName());
+ }
+ if (exp instanceof InputExpression) {
+ candidates.add(((InputExpression) exp).getFieldName());
+ }
+ if (exp instanceof ZCurveExpression) {
+ touchedFields.addAll(candidates);
+ }
+ }
+
+ Collection<String> touchedFields() {
+ Collection<String> output = touchedFields;
+ touchedFields = null; // deny re-use to try and avoid obvious bugs
+ return output;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/Juniperrc.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Juniperrc.java
new file mode 100644
index 00000000000..9ef0ddbc723
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Juniperrc.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.config.search.summary.JuniperrcConfig;
+
+import java.util.Set;
+
+/**
+ * Generated juniperrc-config for controlling juniper.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class Juniperrc extends Derived implements JuniperrcConfig.Producer {
+
+ // List of all fields that should be bolded.
+ private Set<String> boldingFields = new java.util.LinkedHashSet<>();
+
+ /**
+ * Constructs a new juniper rc instance for a given search object. This will derive the configuration automatically,
+ * so there is no need to call {@link #derive(com.yahoo.searchdefinition.Search)}.
+ *
+ * @param search The search model to use for deriving.
+ */
+ public Juniperrc(Search search) {
+ derive(search);
+ }
+
+ // Inherit doc from Derived.
+ @Override
+ protected void derive(Search search) {
+ super.derive(search);
+ for (SummaryField summaryField : search.getUniqueNamedSummaryFields().values()) {
+ if (summaryField.getTransform() == SummaryTransform.BOLDED) {
+ boldingFields.add(summaryField.getName());
+ }
+ }
+ }
+
+ // Inherit doc from Derived.
+ protected String getDerivedName() {
+ return "juniperrc";
+ }
+
+ @Override
+ public void getConfig(JuniperrcConfig.Builder builder) {
+ if (boldingFields.size() != 0) {
+ builder.prefix(true);
+ for (String name : boldingFields) {
+ builder.override(new JuniperrcConfig.Override.Builder()
+ .fieldname(name)
+ .length(65536)
+ .max_matches(1)
+ .min_length(8192)
+ .surround_max(65536));
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinition.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinition.java
new file mode 100644
index 00000000000..c1176807519
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinition.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.document.RankType;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The definition of a rank type used for native rank features.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class NativeRankTypeDefinition {
+
+ /** The type this defines */
+ private RankType type;
+
+ /** The rank tables of this rank type */
+ private List<NativeTable> rankTables = new java.util.ArrayList<>();
+
+ public NativeRankTypeDefinition(RankType type) {
+ this.type = type;
+ }
+
+ public RankType getType() {
+ return type;
+ }
+
+ public void addTable(NativeTable table) {
+ rankTables.add(table);
+ }
+
+ /** Returns an unmodifiable list of the tables in this type definition */
+ public Iterator<NativeTable> rankSettingIterator() {
+ return Collections.unmodifiableList(rankTables).iterator();
+ }
+
+ public String toString() {
+ return "native definition of rank type '" + type + "'";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionSet.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionSet.java
new file mode 100644
index 00000000000..18856627b70
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionSet.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.document.RankType;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A set of rank type definitions used for native rank features.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class NativeRankTypeDefinitionSet {
+
+ /** The name of this rank definition set */
+ private String name;
+
+ /** The unmodifiable rank type implementations in this set */
+ private final Map<RankType, NativeRankTypeDefinition> typeDefinitions;
+
+ /** Returns the default rank type (about) */
+ public static RankType getDefaultRankType() { return RankType.ABOUT; }
+
+ public NativeRankTypeDefinitionSet(String name) {
+ this.name = name;
+
+ Map<RankType, NativeRankTypeDefinition> typeDefinitions = new java.util.LinkedHashMap<>();
+ typeDefinitions.put(RankType.IDENTITY, createIdentityRankType(RankType.IDENTITY));
+ typeDefinitions.put(RankType.ABOUT, createAboutRankType(RankType.ABOUT));
+ typeDefinitions.put(RankType.TAGS, createTagsRankType(RankType.TAGS));
+ typeDefinitions.put(RankType.EMPTY, createEmptyRankType(RankType.EMPTY));
+ this.typeDefinitions = Collections.unmodifiableMap(typeDefinitions);
+ }
+
+ private NativeRankTypeDefinition createEmptyRankType(RankType type) {
+ NativeRankTypeDefinition rank = new NativeRankTypeDefinition(type);
+ rank.addTable(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "linear(0,0)"));
+ rank.addTable(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "linear(0,0)"));
+ rank.addTable(new NativeTable(NativeTable.Type.PROXIMITY, "linear(0,0)"));
+ rank.addTable(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "linear(0,0)"));
+ rank.addTable(new NativeTable(NativeTable.Type.WEIGHT, "linear(0,0)"));
+ return rank;
+ }
+
+ private NativeRankTypeDefinition createAboutRankType(RankType type) {
+ NativeRankTypeDefinition rank = new NativeRankTypeDefinition(type);
+ rank.addTable(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(8000,12.50)"));
+ rank.addTable(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"));
+ rank.addTable(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(500,3)"));
+ rank.addTable(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(400,3)"));
+ rank.addTable(new NativeTable(NativeTable.Type.WEIGHT, "linear(1,0)"));
+ return rank;
+ }
+
+ private NativeRankTypeDefinition createIdentityRankType(RankType type) {
+ NativeRankTypeDefinition rank = new NativeRankTypeDefinition(type);
+ rank.addTable(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(100,12.50)"));
+ rank.addTable(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"));
+ rank.addTable(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(5000,3)"));
+ rank.addTable(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(3000,3)"));
+ rank.addTable(new NativeTable(NativeTable.Type.WEIGHT, "linear(1,0)"));
+ return rank;
+ }
+
+ private NativeRankTypeDefinition createTagsRankType(RankType type) {
+ NativeRankTypeDefinition rank = new NativeRankTypeDefinition(type);
+ rank.addTable(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(8000,12.50)"));
+ rank.addTable(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"));
+ rank.addTable(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(500,3)"));
+ rank.addTable(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(400,3)"));
+ rank.addTable(new NativeTable(NativeTable.Type.WEIGHT, "loggrowth(38,50,1)"));
+ return rank;
+ }
+
+ /**
+ * Returns a rank type definition if given an existing rank type name,
+ * or null if given a rank type which has no native implementation (meaning somebody forgot to add it),
+ */
+ public NativeRankTypeDefinition getRankTypeDefinition(RankType type) {
+ if (type == RankType.DEFAULT)
+ type = getDefaultRankType();
+ return typeDefinitions.get(type);
+ }
+
+ /** Returns an unmodifiable map of the type definitions in this */
+ public Map<RankType, NativeRankTypeDefinition> types() { return typeDefinitions; }
+
+ public String toString() {
+ return "native rank type definitions " + name;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeTable.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeTable.java
new file mode 100644
index 00000000000..512d4a37647
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/NativeTable.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+/**
+ * A named rank table of a certain type.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class NativeTable {
+
+ private String name;
+
+ private Type type;
+
+ /** A table type enumeration */
+ public static class Type {
+
+ public static Type FIRST_OCCURRENCE = new Type("firstOccurrenceTable");
+ public static Type OCCURRENCE_COUNT = new Type("occurrenceCountTable");
+ public static Type WEIGHT = new Type("weightTable");
+ public static Type PROXIMITY = new Type("proximityTable");
+ public static Type REVERSE_PROXIMITY = new Type("reverseProximityTable");
+
+ private String name;
+
+ private Type(String name) {
+ this.name = name;
+ }
+
+ public String getName() { return name; }
+
+ public boolean equals(Object object) {
+ if (!(object instanceof Type)) {
+ return false;
+ }
+ Type other = (Type)object;
+ return this.name.equals(other.name);
+ }
+
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ public String toString() {
+ return getName();
+ }
+ }
+
+ public NativeTable(Type type, String name) {
+ this.type = type;
+ this.name = name;
+ }
+
+ public Type getType() { return type; }
+
+ public String getName() { return name; }
+
+ public int hashCode() {
+ return type.hashCode() + 17*name.hashCode();
+ }
+
+ public boolean equals(Object object) {
+ if (! (object instanceof NativeTable)) return false;
+ NativeTable other = (NativeTable)object;
+ return other.getName().equals(this.getName()) && other.getType().equals(this.getType());
+ }
+
+ public String toString() {
+ return getType() + ": " + getName();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/RankProfileList.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/RankProfileList.java
new file mode 100644
index 00000000000..183bfd6ddd4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/RankProfileList.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.Search;
+import java.util.Map;
+
+/**
+ * The derived rank profiles of a search definition
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class RankProfileList extends Derived implements RankProfilesConfig.Producer {
+
+ private RawRankProfile defaultProfile;
+
+ private Map<String, RawRankProfile> rankProfiles=new java.util.LinkedHashMap<>();
+
+ /**
+ * Creates a rank profile
+ *
+ * @param search the search definition this is a rank profile from
+ * @param attributeFields the attribute fields to create a ranking for
+ */
+ public RankProfileList(Search search, AttributeFields attributeFields, RankProfileRegistry rankProfileRegistry) {
+ setName(search.getName());
+ deriveRankProfiles(rankProfileRegistry, search, attributeFields);
+ }
+
+ private void deriveRankProfiles(RankProfileRegistry rankProfileRegistry, Search search, AttributeFields attributeFields) {
+ defaultProfile = new RawRankProfile(rankProfileRegistry.getRankProfile(search, "default"), attributeFields);
+ rankProfiles.put(defaultProfile.getName(), defaultProfile);
+
+ for (RankProfile rank : rankProfileRegistry.localRankProfiles(search)) {
+ if ("default".equals(rank.getName()))
+ continue;
+ RawRankProfile rawRank=new RawRankProfile(rank, attributeFields);
+ rankProfiles.put(rawRank.getName(), rawRank);
+ }
+ }
+
+ public Map<String, RawRankProfile> getRankProfiles() {
+ return rankProfiles;
+ }
+
+ /** @return A named raw rank profile, or null if it is not present */
+ public RawRankProfile getRankProfile(String name) {
+ return rankProfiles.get(name);
+ }
+
+ @Override
+ public String getDerivedName() { return "rank-profiles"; }
+
+ @Override
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ for (RawRankProfile rank : rankProfiles.values() ) {
+ rank.getConfig(builder);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java
new file mode 100644
index 00000000000..cf3b10ccadd
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/RawRankProfile.java
@@ -0,0 +1,374 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchlib.rankingexpression.ExpressionFunction;
+import com.yahoo.searchlib.rankingexpression.RankingExpression;
+import com.yahoo.searchlib.rankingexpression.parser.ParseException;
+import com.yahoo.searchlib.rankingexpression.rule.ReferenceNode;
+import com.yahoo.searchlib.rankingexpression.rule.SerializationContext;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import java.util.*;
+
+/**
+ * A rank profile derived from a search definition, containing exactly the features available natively in the server
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class RawRankProfile implements RankProfilesConfig.Producer {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public String toString() {
+ return " rank profile " + name;
+ }
+
+ @Override
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ RankProfilesConfig.Rankprofile.Builder b = new RankProfilesConfig.Rankprofile.Builder().name(getName());
+ getRankProperties(b);
+ builder.rankprofile(b);
+ }
+
+ private void getRankProperties(RankProfilesConfig.Rankprofile.Builder b) {
+ RankProfilesConfig.Rankprofile.Fef.Builder fefB = new RankProfilesConfig.Rankprofile.Fef.Builder();
+ for (Map.Entry<String, Object> e : configProperties.entrySet()) {
+ String key = e.getKey().replaceFirst(".part\\d+$", "");
+ String val = e.getValue().toString();
+ fefB.property(new RankProfilesConfig.Rankprofile.Fef.Property.Builder().name(key).value(val));
+ }
+ b.fef(fefB);
+ }
+
+ // TODO: These are to expose coupling between the strings used here and elsewhere
+ public final static String summaryFeatureFefPropertyPrefix = "vespa.summary.feature";
+ public final static String rankFeatureFefPropertyPrefix = "vespa.dump.feature";
+
+ /**
+ * Returns an immutable view of the config properties this returns
+ */
+ public Map<String, Object> configProperties() {
+ return Collections.unmodifiableMap(configProperties);
+ }
+
+ private final Map<String, Object> configProperties;
+
+ /**
+ * Creates a raw rank profile from the given rank profile
+ */
+ public RawRankProfile(RankProfile rankProfile, AttributeFields attributeFields) {
+ this.name = rankProfile.getName();
+ configProperties = new Deriver(rankProfile, attributeFields).derive();
+ }
+
+ private static class Deriver {
+
+ /**
+ * The field rank settings of this profile
+ */
+ private Map<String, FieldRankSettings> fieldRankSettings = new java.util.LinkedHashMap<>();
+
+ private RankingExpression firstPhaseRanking = null;
+
+ private RankingExpression secondPhaseRanking = null;
+
+ private Set<ReferenceNode> summaryFeatures = new LinkedHashSet<>();
+
+ private Set<ReferenceNode> rankFeatures = new LinkedHashSet<>();
+
+ private List<RankProfile.RankProperty> rankProperties = new ArrayList<>();
+
+ /**
+ * Rank properties for weight settings to make these available to feature executors
+ */
+ private List<RankProfile.RankProperty> boostAndWeightRankProperties = new ArrayList<>();
+
+ private boolean ignoreDefaultRankFeatures = false;
+
+ private RankProfile.MatchPhaseSettings matchPhaseSettings = null;
+
+ private int rerankCount = -1;
+ private int keepRankCount = -1;
+ private int numThreadsPerSearch = -1;
+ private int numSearchPartitions = -1;
+ private double termwiseLimit = 1.0;
+ private double rankScoreDropLimit = -Double.MAX_VALUE;
+
+ /**
+ * The rank type definitions used to derive settings for the native rank features
+ */
+ private NativeRankTypeDefinitionSet nativeRankTypeDefinitions = new NativeRankTypeDefinitionSet("default");
+
+ private RankProfile rankProfile;
+
+ private Set<String> filterFields = new java.util.LinkedHashSet<>();
+
+ /**
+ * Creates a raw rank profile from the given rank profile
+ */
+ public Deriver(RankProfile rankProfile, AttributeFields attributeFields) {
+ this.rankProfile = rankProfile.compile();
+ deriveRankingFeatures(this.rankProfile);
+ deriveRankTypeSetting(this.rankProfile, attributeFields);
+ deriveFilterFields(this.rankProfile);
+ deriveWeightProperties(this.rankProfile);
+ }
+
+ private void deriveFilterFields(RankProfile rp) {
+ filterFields.addAll(rp.allFilterFields());
+ }
+
+ public void deriveRankingFeatures(RankProfile rankProfile) {
+ firstPhaseRanking = rankProfile.getFirstPhaseRanking();
+ secondPhaseRanking = rankProfile.getSecondPhaseRanking();
+ summaryFeatures = new LinkedHashSet<>(rankProfile.getSummaryFeatures());
+ rankFeatures = rankProfile.getRankFeatures();
+ rerankCount = rankProfile.getRerankCount();
+ matchPhaseSettings = rankProfile.getMatchPhaseSettings();
+ numThreadsPerSearch = rankProfile.getNumThreadsPerSearch();
+ numSearchPartitions = rankProfile.getNumSearchPartitions();
+ termwiseLimit = rankProfile.getTermwiseLimit();
+ keepRankCount = rankProfile.getKeepRankCount();
+ rankScoreDropLimit = rankProfile.getRankScoreDropLimit();
+ ignoreDefaultRankFeatures = rankProfile.getIgnoreDefaultRankFeatures();
+ rankProperties = new ArrayList<>(rankProfile.getRankProperties());
+ derivePropertiesAndSummaryFeaturesFromMacros(rankProfile.getMacros());
+ }
+
+ private void derivePropertiesAndSummaryFeaturesFromMacros(Map<String, RankProfile.Macro> macros) {
+ if (macros.isEmpty()) return;
+ Map<String, ExpressionFunction> expressionMacros = new LinkedHashMap<>();
+ for (Map.Entry<String, RankProfile.Macro> macro : macros.entrySet()) {
+ expressionMacros.put(macro.getKey(), macro.getValue().toExpressionMacro());
+ }
+
+ Map<String, String> macroProperties = new LinkedHashMap<>();
+ macroProperties.putAll(deriveMacroProperties(expressionMacros));
+ if (firstPhaseRanking != null) {
+ macroProperties.putAll(firstPhaseRanking.getRankProperties(new ArrayList<>(expressionMacros.values())));
+ }
+ if (secondPhaseRanking != null) {
+ macroProperties.putAll(secondPhaseRanking.getRankProperties(new ArrayList<>(expressionMacros.values())));
+ }
+ for (Map.Entry<String, String> e : macroProperties.entrySet()) {
+ rankProperties.add(new RankProfile.RankProperty(e.getKey(), e.getValue()));
+ }
+ SerializationContext context = new SerializationContext(expressionMacros.values(), null, macroProperties);
+ replaceMacroSummaryFeatures(context);
+ }
+
+ private Map<String, String> deriveMacroProperties(Map<String, ExpressionFunction> eMacros) {
+ SerializationContext context = new SerializationContext(eMacros);
+ for (Map.Entry<String, ExpressionFunction> e : eMacros.entrySet()) {
+ String script = e.getValue().getBody().getRoot().toString(context, null, null);
+ context.addFunctionSerialization(RankingExpression.propertyName(e.getKey()), script);
+ }
+ return context.serializedFunctions();
+ }
+
+ private void replaceMacroSummaryFeatures(SerializationContext context) {
+ if (summaryFeatures == null) return;
+ Map<String, ReferenceNode> macroSummaryFeatures = new LinkedHashMap<>();
+ for (Iterator<ReferenceNode> i = summaryFeatures.iterator(); i.hasNext(); ) {
+ ReferenceNode referenceNode = i.next();
+ // Is the feature a macro?
+ if (context.getFunction(referenceNode.getName()) != null) {
+ context.addFunctionSerialization(RankingExpression.propertyName(referenceNode.getName()),
+ referenceNode.toString(context, null, null));
+ ReferenceNode newReferenceNode = new ReferenceNode("rankingExpression(" + referenceNode.getName() + ")", referenceNode.getArguments().expressions(), referenceNode.getOutput());
+ macroSummaryFeatures.put(referenceNode.getName(), newReferenceNode);
+ i.remove(); // Will add the expanded one in next block
+ }
+ }
+ // Then, replace the summary features that were macros
+ for (Map.Entry<String, ReferenceNode> e : macroSummaryFeatures.entrySet()) {
+ summaryFeatures.add(e.getValue());
+ }
+ }
+
+ private void deriveWeightProperties(RankProfile rankProfile) {
+
+ for (RankProfile.RankSetting setting : rankProfile.rankSettings()) {
+ if (!setting.getType().equals(RankProfile.RankSetting.Type.WEIGHT)) {
+ continue;
+ }
+ boostAndWeightRankProperties.add(new RankProfile.RankProperty("vespa.fieldweight." + setting.getFieldName(),
+ String.valueOf(setting.getIntValue())));
+ }
+ }
+
+ /**
+ * Adds the type boosts from a rank profile
+ */
+ private void deriveRankTypeSetting(RankProfile rankProfile, AttributeFields attributeFields) {
+ for (Iterator<RankProfile.RankSetting> i = rankProfile.rankSettingIterator(); i.hasNext(); ) {
+ RankProfile.RankSetting setting = i.next();
+ if (!setting.getType().equals(RankProfile.RankSetting.Type.RANKTYPE)) continue;
+
+ deriveNativeRankTypeSetting(setting.getFieldName(), (RankType) setting.getValue(), attributeFields,
+ hasDefaultRankTypeSetting(rankProfile, setting.getFieldName()));
+ }
+ }
+
+ public void deriveNativeRankTypeSetting(String fieldName, RankType rankType, AttributeFields attributeFields, boolean isDefaultSetting) {
+ if (isDefaultSetting) return;
+
+ NativeRankTypeDefinition definition = nativeRankTypeDefinitions.getRankTypeDefinition(rankType);
+ if (definition == null) throw new IllegalArgumentException("In field '" + fieldName + "': " +
+ rankType + " is known but has no implementation. " +
+ "Supported rank types: " +
+ nativeRankTypeDefinitions.types().keySet());
+
+ FieldRankSettings settings = deriveFieldRankSettings(fieldName);
+ for (Iterator<NativeTable> i = definition.rankSettingIterator(); i.hasNext(); ) {
+ NativeTable table = i.next();
+ // only add index field tables if we are processing an index field and
+ // only add attribute field tables if we are processing an attribute field
+ if ((FieldRankSettings.isIndexFieldTable(table) && attributeFields.getAttribute(fieldName) == null) ||
+ (FieldRankSettings.isAttributeFieldTable(table) && attributeFields.getAttribute(fieldName) != null)) {
+ settings.addTable(table);
+ }
+ }
+ }
+
+ private boolean hasDefaultRankTypeSetting(RankProfile rankProfile, String fieldName) {
+ RankProfile.RankSetting setting =
+ rankProfile.getRankSetting(fieldName, RankProfile.RankSetting.Type.RANKTYPE);
+ return setting != null && setting.getValue().equals(RankType.DEFAULT);
+ }
+
+ public FieldRankSettings deriveFieldRankSettings(String fieldName) {
+ FieldRankSettings settings = fieldRankSettings.get(fieldName);
+ if (settings == null) {
+ settings = new FieldRankSettings(fieldName);
+ fieldRankSettings.put(fieldName, settings);
+ }
+ return settings;
+ }
+
+ /**
+ * Derives the properties this produces. Equal keys are suffixed with .part0 etc, remove when exporting to file
+ *
+ * @return map of the derived properties
+ */
+ public Map<String, Object> derive() {
+ Map<String, Object> props = new LinkedHashMap<>();
+ int i = 0;
+ for (RankProfile.RankProperty property : rankProperties) {
+ if ("rankingExpression(firstphase).rankingScript".equals(property.getName())) {
+ // Could have been set by macro expansion. Set expressions, then skip this property.
+ try {
+ firstPhaseRanking = new RankingExpression(property.getValue());
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Could not parse second phase expression", e);
+ }
+ continue;
+ }
+ if ("rankingExpression(secondphase).rankingScript".equals(property.getName())) {
+ try {
+ secondPhaseRanking = new RankingExpression(property.getValue());
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Could not parse second phase expression", e);
+ }
+ continue;
+ }
+ props.put(property.getName() + ".part" + i, property.getValue());
+ i++;
+ }
+ props.putAll(deriveRankingPhaseRankProperties(firstPhaseRanking, "firstphase"));
+ props.putAll(deriveRankingPhaseRankProperties(secondPhaseRanking, "secondphase"));
+ for (FieldRankSettings settings : fieldRankSettings.values()) {
+ props.putAll(settings.deriveRankProperties(i));
+ }
+ i = 0;
+ for (RankProfile.RankProperty property : boostAndWeightRankProperties) {
+ props.put(property.getName() + ".part" + i, property.getValue());
+ i++;
+ }
+ i = 0;
+ for (ReferenceNode feature : summaryFeatures) {
+ props.put(summaryFeatureFefPropertyPrefix + ".part" + i, feature.toString());
+ i++;
+ }
+ i = 0;
+ for (ReferenceNode feature : rankFeatures) {
+ props.put(rankFeatureFefPropertyPrefix + ".part" + i, feature.toString());
+ i++;
+ }
+ if (numThreadsPerSearch > 0) {
+ props.put("vespa.matching.numthreadspersearch", numThreadsPerSearch + "");
+ }
+ if (numSearchPartitions >= 0) {
+ props.put("vespa.matching.numsearchpartitions", numSearchPartitions + "");
+ }
+ if (termwiseLimit < 1.0) {
+ props.put("vespa.matching.termwise_limit", termwiseLimit + "");
+ }
+ if (matchPhaseSettings != null) {
+ props.put("vespa.matchphase.degradation.attribute", matchPhaseSettings.getAttribute());
+ props.put("vespa.matchphase.degradation.ascendingorder", matchPhaseSettings.getAscending() + "");
+ props.put("vespa.matchphase.degradation.maxhits", matchPhaseSettings.getMaxHits() + "");
+ props.put("vespa.matchphase.degradation.maxfiltercoverage", matchPhaseSettings.getMaxFilterCoverage() + "");
+ props.put("vespa.matchphase.degradation.samplepercentage", matchPhaseSettings.getEvaluationPoint() + "");
+ props.put("vespa.matchphase.degradation.postfiltermultiplier", matchPhaseSettings.getPrePostFilterTippingPoint() + "");
+ RankProfile.DiversitySettings diversitySettings = rankProfile.getMatchPhaseSettings().getDiversity();
+ if (diversitySettings != null) {
+ props.put("vespa.matchphase.diversity.attribute", diversitySettings.getAttribute());
+ props.put("vespa.matchphase.diversity.mingroups", diversitySettings.getMinGroups());
+ props.put("vespa.matchphase.diversity.cutoff.factor", diversitySettings.getCutoffFactor());
+ props.put("vespa.matchphase.diversity.cutoff.strategy", diversitySettings.getCutoffStrategy());
+ }
+ }
+ if (rerankCount > -1) {
+ props.put("vespa.hitcollector.heapsize", rerankCount + "");
+ }
+ if (keepRankCount > -1) {
+ props.put("vespa.hitcollector.arraysize", keepRankCount + "");
+ }
+ if (rankScoreDropLimit > -Double.MAX_VALUE) {
+ props.put("vespa.hitcollector.rankscoredroplimit", rankScoreDropLimit + "");
+ }
+ if (ignoreDefaultRankFeatures) {
+ props.put("vespa.dump.ignoredefaultfeatures", true);
+ }
+ Iterator filterFieldsIterator = filterFields.iterator();
+ while (filterFieldsIterator.hasNext()) {
+ String fieldName = (String) filterFieldsIterator.next();
+ props.put("vespa.isfilterfield." + fieldName + ".part42", true);
+ }
+ for (Map.Entry<String, String> attributeType : rankProfile.getAttributeTypes().entrySet()) {
+ props.put("vespa.type.attribute." + attributeType.getKey(), attributeType.getValue());
+ }
+ for (Map.Entry<String, String> queryFeatureType : rankProfile.getQueryFeatureTypes().entrySet()) {
+ props.put("vespa.type.query." + queryFeatureType.getKey(), queryFeatureType.getValue());
+ }
+ if (props.size() >= 1000000) throw new RuntimeException("Too many rank properties");
+ return props;
+ }
+
+ private Map<String, String> deriveRankingPhaseRankProperties(RankingExpression expression, String phase) {
+ Map<String, String> ret = new LinkedHashMap<>();
+ if (expression == null) {
+ return ret;
+ }
+ String name = expression.getName();
+ if ("".equals(name)) {
+ name = phase;
+ }
+ if (expression.getRoot() instanceof ReferenceNode) {
+ ret.put("vespa.rank." + phase, expression.getRoot().toString());
+ } else {
+ ret.put("vespa.rank." + phase, "rankingExpression(" + name + ")");
+ ret.put("rankingExpression(" + name + ").rankingScript", expression.getRoot().toString());
+ }
+ return ret;
+ }
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/SearchOrderer.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SearchOrderer.java
new file mode 100644
index 00000000000..69133e31fb3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SearchOrderer.java
@@ -0,0 +1,105 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.DataTypeName;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.Search;
+
+import java.util.*;
+
+/**
+ * <p>A class which can reorder a list of search definitions such that any supertype
+ * always preceed any subtype. Subject to this condition the given order
+ * is preserved (the minimal reordering is done).</p>
+ *
+ * <p>This class is <b>not</b> multithread safe. Only one ordering must be done
+ * at the time in any instance.</p>
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class SearchOrderer {
+
+ /** A map from DataTypeName to the Search defining them */
+ private Map<DataTypeName, Search> documentNameToSearch=new java.util.HashMap<>();
+
+ /**
+ * Reorders the given list of search definitions such that any supertype
+ * always preceed any subtype. Subject to this condition the given order
+ * is preserved (the minimal reordering is done).
+ *
+ * @return a new list containing the same search instances in the right order
+ */
+ public List<Search> order(List<Search> unordered) {
+ Collections.sort(unordered, new Comparator<Search>() {
+ @Override
+ public int compare(Search lhs, Search rhs) {
+ return lhs.getName().compareTo(rhs.getName());
+ }
+ });
+
+ // No, this is not a fast algorithm...
+ indexOnDocumentName(unordered);
+ List<Search> ordered=new java.util.ArrayList<>(unordered.size());
+ List<Search> moveOutwards=new java.util.ArrayList<>();
+ for (Search search: unordered) {
+ if (containsInherited(ordered,search)) {
+ addOrdered(ordered,search,moveOutwards);
+ }
+ else {
+ moveOutwards.add(search);
+ }
+ }
+
+ // Any leftovers means we have search definitions with undefined inheritants.
+ // This is warned about elsewhere.
+ ordered.addAll(moveOutwards);
+
+ documentNameToSearch.clear();
+ return ordered;
+ }
+
+ private void addOrdered(List<Search> ordered,Search search,List<Search> moveOutwards) {
+ ordered.add(search);
+ Search eligibleMove;
+ do {
+ eligibleMove=removeFirstEligibleMoveOutwards(moveOutwards,ordered);
+ if (eligibleMove!=null)
+ ordered.add(eligibleMove);
+ } while (eligibleMove!=null);
+ }
+
+ /** Removes and returns the first search from the move list which can now be added, or null if none */
+ private Search removeFirstEligibleMoveOutwards(List<Search> moveOutwards,List<Search> ordered) {
+ for (Search move : moveOutwards) {
+ if (containsInherited(ordered,move)) {
+ moveOutwards.remove(move);
+ return move;
+ }
+ }
+ return null;
+ }
+
+ private boolean containsInherited(List<Search> list,Search search) {
+ if (search.getDocument() == null) {
+ return true;
+ }
+ for (SDDocumentType sdoc : search.getDocument().getInheritedTypes() ) {
+ DataTypeName inheritedName=sdoc.getDocumentName();
+ if ("document".equals(inheritedName.getName())) continue;
+ Search inheritedSearch=documentNameToSearch.get(inheritedName);
+ if (!list.contains(inheritedSearch))
+ return false;
+ }
+ return true;
+ }
+
+ private void indexOnDocumentName(List<Search> searches) {
+ documentNameToSearch.clear();
+ for (Search search : searches) {
+ if (search.getDocument() != null) {
+ documentNameToSearch.put(search.getDocument().getDocumentName(),search);
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/Summaries.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Summaries.java
new file mode 100644
index 00000000000..357e0d40f49
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/Summaries.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.config.search.SummaryConfig;
+import java.util.List;
+
+/**
+ * A list of derived summaries
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class Summaries extends Derived implements SummaryConfig.Producer {
+
+ private List<SummaryClass> summaries=new java.util.ArrayList<>(1);
+
+ public Summaries(Search search, DeployLogger deployLogger) {
+ // Make sure the default is first
+ summaries.add(new SummaryClass(search,search.getSummary("default"), deployLogger));
+ for (DocumentSummary summary : search.getSummaries().values()) {
+ if (!summary.getName().equals("default"))
+ summaries.add(new SummaryClass(search,summary, deployLogger));
+ }
+ }
+
+ protected String getDerivedName() { return "summary"; }
+
+ @Override
+ public void getConfig(SummaryConfig.Builder builder) {
+ builder.defaultsummaryid(summaries.isEmpty() ? -1 : summaries.get(0).hashCode());
+ for (SummaryClass summaryClass : summaries) {
+ builder.classes(summaryClass.getSummaryClassConfig());
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClass.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClass.java
new file mode 100644
index 00000000000..d21523caea2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClass.java
@@ -0,0 +1,144 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.prelude.fastsearch.DocsumDefinitionSet;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.config.search.SummaryConfig;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Random;
+import java.util.logging.Level;
+
+/**
+ * A summary derived from a search definition.
+ * Each summary definition have at least one summary, the default
+ * which has the same name as the search definition.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class SummaryClass extends Derived {
+
+ /** True if this summary class needs to access summary information on disk */
+ private boolean accessingDiskSummary=false;
+
+ /** The summary fields of this indexed by name */
+ private Map<String,SummaryClassField> fields = new java.util.LinkedHashMap<>();
+
+ private DeployLogger deployLogger = new BaseDeployLogger();
+
+ private final Random random = new Random(7);
+
+ /**
+ * Creates a summary class from a search definition summary
+ *
+ * @param deployLogger a {@link DeployLogger}
+ */
+ public SummaryClass(Search search, DocumentSummary summary, DeployLogger deployLogger) {
+ this.deployLogger = deployLogger;
+ deriveName(summary);
+ deriveFields(search,summary);
+ deriveImplicitFields(summary);
+ }
+
+ private void deriveName(DocumentSummary summary) {
+ setName(summary.getName());
+ }
+
+ /** MUST be called after all other fields are added */
+ private void deriveImplicitFields(DocumentSummary summary) {
+ if (summary.getName().equals("default")) {
+ addField("documentid", DataType.STRING);
+ }
+ }
+
+ private void deriveFields(Search search, DocumentSummary summary) {
+ for (SummaryField summaryField : summary.getSummaryFields()) {
+ if (!accessingDiskSummary && search.isAccessingDiskSummary(summaryField)) {
+ accessingDiskSummary = true;
+ }
+ addField(summaryField.getName(), summaryField.getDataType(), summaryField.getTransform());
+ }
+ }
+
+ private void addField(String name, DataType type) {
+ addField(name, type, null);
+ }
+
+ private void addField(String name, DataType type, SummaryTransform transform) {
+ if (fields.containsKey(name)) {
+ SummaryClassField sf = fields.get(name);
+ if (!SummaryClassField.convertDataType(type, transform).equals(sf.getType())) {
+ deployLogger.log(Level.WARNING, "Conflicting definition of field " + name + ". " +
+ "Declared as type " + sf.getType() + " and " +
+ type);
+ }
+ } else {
+ fields.put(name, new SummaryClassField(name, type, transform));
+ }
+ }
+
+
+ /** Returns an iterator of the fields of this summary. Removes on this iterator removes the field from this summary */
+ public Iterator<SummaryClassField> fieldIterator() {
+ return fields.values().iterator();
+ }
+
+ public void addField(SummaryClassField field) {
+ fields.put(field.getName(),field);
+ }
+
+ /** Returns the writable map of fields of this summary */ // TODO: Make read only, move writers to iterator/addField
+ public Map<String,SummaryClassField> getFields() { return fields; }
+
+ public SummaryClassField getField(String name) {
+ return fields.get(name);
+ }
+
+ public int getFieldCount() { return fields.size(); }
+
+ public int hashCode() {
+ int number = 1;
+ int hash = getName().hashCode();
+ for (Iterator i = fieldIterator(); i.hasNext(); ) {
+ SummaryClassField field = (SummaryClassField)i.next();
+ hash += number * (field.getName().hashCode() +
+ 17*field.getType().getName().hashCode());
+ number++;
+ }
+ if (hash < 0)
+ hash *= -1;
+ return hash;
+ }
+
+ public SummaryConfig.Classes.Builder getSummaryClassConfig() {
+ SummaryConfig.Classes.Builder classBuilder = new SummaryConfig.Classes.Builder();
+ int id = hashCode();
+ if (id == DocsumDefinitionSet.SLIME_MAGIC_ID) {
+ deployLogger.log(Level.WARNING, "Summary class '" + getName() + "' hashes to the SLIME_MAGIC_ID '" + id +
+ "'. This is unlikely but I autofix it for you by adding a random number.");
+ id += random.nextInt();
+ }
+ classBuilder.
+ id(id).
+ name(getName());
+ for (SummaryClassField field : fields.values() ) {
+ classBuilder.fields(new SummaryConfig.Classes.Fields.Builder().
+ name(field.getName()).
+ type(field.getType().getName()));
+ }
+ return classBuilder;
+ }
+
+ protected String getDerivedName() { return "summary"; }
+
+ public String toString() {
+ return "summary class " + getName();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClassField.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClassField.java
new file mode 100644
index 00000000000..bb1dd87f314
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryClassField.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.document.MapDataType;
+import com.yahoo.document.datatypes.*;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+
+/**
+ * A summary field derived from a search definition
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class SummaryClassField {
+
+ private final String name;
+
+ private final Type type;
+
+ /** The summary field type enumeration */
+ public enum Type {
+
+ BYTE("byte"),
+ SHORT("short"),
+ INTEGER("integer"),
+ INT64("int64"),
+ FLOAT("float"),
+ DOUBLE("double"),
+ STRING("string"),
+ DATA("data"),
+ LONGSTRING("longstring"),
+ LONGDATA("longdata"),
+ XMLSTRING("xmlstring"),
+ FEATUREDATA("featuredata"),
+ JSONSTRING("jsonstring");
+
+ private String name;
+
+ private Type(String name) {
+ this.name = name;
+ }
+
+ /** Returns the name of this type */
+ public String getName() {
+ return name;
+ }
+
+ public String toString() {
+ return "type: " + name;
+ }
+ }
+
+ public SummaryClassField(String name, DataType type, SummaryTransform transform) {
+ this.name = name;
+ this.type = convertDataType(type, transform);
+ }
+
+ public String getName() { return name; }
+
+ public Type getType() { return type; }
+
+ /** Converts to the right summary field type from a field datatype and a transform*/
+ public static Type convertDataType(DataType fieldType, SummaryTransform transform) {
+ FieldValue fval = fieldType.createFieldValue();
+ if (fval instanceof StringFieldValue) {
+ if (transform != null && transform.equals(SummaryTransform.RANKFEATURES)) {
+ return Type.FEATUREDATA;
+ } else if (transform != null && transform.equals(SummaryTransform.SUMMARYFEATURES)) {
+ return Type.FEATUREDATA;
+ } else {
+ return Type.LONGSTRING;
+ }
+ } else if (fval instanceof IntegerFieldValue) {
+ return Type.INTEGER;
+ } else if (fval instanceof LongFieldValue) {
+ return Type.INT64;
+ } else if (fval instanceof FloatFieldValue) {
+ return Type.FLOAT;
+ } else if (fval instanceof DoubleFieldValue) {
+ return Type.DOUBLE;
+ } else if (fval instanceof ByteFieldValue) {
+ return Type.BYTE;
+ } else if (fval instanceof Raw) {
+ return Type.DATA;
+ } else if (fval instanceof Struct) {
+ return Type.JSONSTRING;
+ } else if (fval instanceof PredicateFieldValue) {
+ return Type.STRING;
+ } else if (fval instanceof TensorFieldValue) {
+ return Type.JSONSTRING;
+ } else if (fieldType instanceof CollectionDataType) {
+ if (transform != null && transform.equals(SummaryTransform.POSITIONS)) {
+ return Type.XMLSTRING;
+ } else {
+ return Type.JSONSTRING;
+ }
+ } else if (fieldType instanceof MapDataType) {
+ return Type.JSONSTRING;
+ } else {
+ throw new IllegalArgumentException("Don't know which summary type to " +
+ "convert " + fieldType + " to");
+ }
+ }
+
+ public String toString() {
+ return "summary class field " + name;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryMap.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryMap.java
new file mode 100644
index 00000000000..d59c671e6a5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/SummaryMap.java
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.config.search.SummarymapConfig;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A summary map (describing search-time summary field transformations)
+ * derived from a search definition
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class SummaryMap extends Derived implements SummarymapConfig.Producer {
+
+ private Map<String,FieldResultTransform> resultTransforms = new java.util.LinkedHashMap<>();
+
+ /** Crateate a summary map from a search definition */
+ public SummaryMap(Search search, Summaries summaries) {
+ derive(search, summaries);
+ }
+
+ protected void derive(Search search, Summaries summaries) {
+ // TODO: This should really derive from the 'summaries' argument. Bug?
+ for (DocumentSummary documentSummary : search.getSummaries().values()) {
+ derive(documentSummary);
+ }
+ super.derive(search);
+ }
+
+ protected void derive(SDField field, Search search) {
+ }
+
+ private void derive(DocumentSummary documentSummary) {
+ for (SummaryField summaryField : documentSummary.getSummaryFields()) {
+ if (summaryField.getTransform()== SummaryTransform.NONE) continue;
+
+ if (summaryField.getTransform()==SummaryTransform.ATTRIBUTE ||
+ summaryField.getTransform()==SummaryTransform.DISTANCE ||
+ summaryField.getTransform()==SummaryTransform.GEOPOS ||
+ summaryField.getTransform()==SummaryTransform.POSITIONS) {
+ resultTransforms.put(summaryField.getName(),new FieldResultTransform(summaryField.getName(),
+ summaryField.getTransform(),
+ summaryField.getSingleSource()));
+ } else {
+ // Note: Currently source mapping is handled in the indexing statement,
+ // by creating a summary field for each of the values
+ // This works, but is suboptimal. We could consolidate to a minimal set and
+ // use the right value from the minimal set as the third parameter here,
+ // and add "override" commands to multiple static values
+ resultTransforms.put(summaryField.getName(),new FieldResultTransform(summaryField.getName(),
+ summaryField.getTransform(),
+ summaryField.getName()));
+ }
+ }
+ }
+
+ /** Returns a read-only iterator of the FieldResultTransforms of this summary map */
+ public Iterator resultTransformIterator() {
+ return Collections.unmodifiableCollection(resultTransforms.values()).iterator();
+ }
+
+ protected String getDerivedName() { return "summarymap"; }
+
+ /** Returns the command name of a transform */
+ private String getCommand(SummaryTransform transform) {
+ if (transform.equals(SummaryTransform.DISTANCE))
+ return "absdist";
+ else if (transform.isDynamic())
+ return "dynamicteaser";
+ else
+ return transform.getName();
+ }
+
+ /**
+ * Does this summary command name stand for a dynamic transform?
+ * We need this because some model information is shared through configs instead of model - see usage
+ */
+ public static boolean isDynamicCommand(String commandName) {
+ return (commandName.equals("dynamicteaser") || commandName.equals("smartsummary"));
+ }
+
+ @Override
+ public void getConfig(SummarymapConfig.Builder builder) {
+ builder.defaultoutputclass(-1);
+ for (FieldResultTransform frt : resultTransforms.values()) {
+ SummarymapConfig.Override.Builder oB = new SummarymapConfig.Override.Builder()
+ .field(frt.getFieldName())
+ .command(getCommand(frt.getTransform()));
+ if (frt.getTransform().isDynamic() ||
+ frt.getTransform().equals(SummaryTransform.ATTRIBUTE) ||
+ frt.getTransform().equals(SummaryTransform.DISTANCE) ||
+ frt.getTransform().equals(SummaryTransform.GEOPOS) ||
+ frt.getTransform().equals(SummaryTransform.POSITIONS) ||
+ frt.getTransform().equals(SummaryTransform.TEXTEXTRACTOR))
+ {
+ oB.arguments(frt.getArgument());
+ } else {
+ oB.arguments("");
+ }
+ builder.override(oB);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmFields.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmFields.java
new file mode 100644
index 00000000000..65698675884
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmFields.java
@@ -0,0 +1,275 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.*;
+import com.yahoo.document.datatypes.*;
+import com.yahoo.searchdefinition.FieldSets;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.FieldSet;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.config.search.vsm.VsmfieldsConfig;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Vertical streaming matcher field specification
+ */
+public class VsmFields extends Derived implements VsmfieldsConfig.Producer {
+
+ private final Map<String, StreamingField> fields=new LinkedHashMap<>();
+ private final Map<String, StreamingDocumentType> doctypes=new LinkedHashMap<>();
+
+ public VsmFields(Search search) {
+ addSearchdefinition(search);
+ }
+
+ private void addSearchdefinition(Search search) {
+ derive(search);
+ }
+
+ protected void derive(SDDocumentType document,Search search) {
+ super.derive(document, search);
+ StreamingDocumentType docType=getDocumentType(document.getName());
+ if (docType == null) {
+ docType = new StreamingDocumentType(document.getName(), search.fieldSets());
+ doctypes.put(document.getName(), docType);
+ }
+ for (Object o : document.fieldSet()) {
+ derive(docType, (SDField) o);
+ }
+ }
+
+ protected void derive(StreamingDocumentType document, SDField field) {
+ if (field.usesStructOrMap()) {
+ for (SDField structField : field.getStructFields()) {
+ derive(document, structField); // Recursion
+ }
+ } else {
+
+ if (! (field.doesIndexing() || field.doesSummarying() || field.doesAttributing()) )
+ return;
+
+ StreamingField streamingField=new StreamingField(field);
+ addField(streamingField.getName(),streamingField);
+ deriveIndices(document, field, streamingField);
+ }
+ }
+
+ private void deriveIndices(StreamingDocumentType document, SDField field, StreamingField streamingField) {
+ if (field.doesIndexing()) {
+ addFieldToIndices(document, field.getName(), streamingField);
+ } else if (field.doesAttributing()) {
+ for (String indexName : field.getAttributes().keySet()) {
+ addFieldToIndices(document, indexName, streamingField);
+ }
+ }
+ }
+
+ private void addFieldToIndices(StreamingDocumentType document, String indexName, StreamingField streamingField) {
+ if (indexName.contains(".")) {
+ addFieldToIndices(document, indexName.substring(0,indexName.lastIndexOf(".")), streamingField); // Recursion
+ }
+ document.addIndexField(indexName, streamingField.getName());
+ }
+
+ private void addField(String name, StreamingField field) {
+ fields.put(name, field);
+ }
+
+ /** Returns a streaming index, or null if there is none with this name */
+ public StreamingDocumentType getDocumentType(String name) {
+ return doctypes.get(name);
+ }
+
+ public String getDerivedName() {
+ return "vsmfields";
+ }
+
+ @Override
+ public void getConfig(VsmfieldsConfig.Builder vsB) {
+ for (StreamingField streamingField : fields.values()) {
+ vsB.fieldspec(streamingField.getFieldSpecConfig());
+ }
+ for (StreamingDocumentType streamingDocType : doctypes.values()) {
+ vsB.documenttype(streamingDocType.getDocTypeConfig());
+ }
+ }
+
+ private static class StreamingField {
+
+ private final String name;
+
+ /** Whether this field does prefix matching by default */
+ private final Matching matching;
+
+ /** The type of this field */
+ private final Type type;
+
+ private final boolean isAttribute;
+
+ /** The streaming field type enumeration */
+ public static class Type {
+
+ public static Type INT8=new Type("int8","INT8");
+ public static Type INT16=new Type("int16","INT16");
+ public static Type INT32=new Type("int32","INT32");
+ public static Type INT64=new Type("int64","INT64");
+ public static Type FLOAT=new Type("float","FLOAT");
+ public static Type DOUBLE=new Type("double","DOUBLE");
+ public static Type STRING=new Type("string","AUTOUTF8");
+ public static Type UNSEARCHABLESTRING=new Type("string","NONE");
+
+ private String name;
+
+ private String searchMethod;
+
+ private Type(String name,String searchMethod) {
+ this.name=name;
+ this.searchMethod=searchMethod;
+ }
+
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ /** Returns the name of this type */
+ public String getName() { return name; }
+
+ public String getSearchMethod() { return searchMethod; }
+
+ public boolean equals(Object other) {
+ if ( ! (other instanceof Type)) return false;
+ return this.name.equals(((Type)other).name);
+ }
+
+ public String toString() {
+ return "type: " + name;
+ }
+
+ }
+
+ public StreamingField(SDField field) {
+ this(field.getName(),field.getDataType(),field.getMatching(), field.doesAttributing());
+ }
+
+ private StreamingField(String name,DataType sourceType, Matching matching, boolean isAttribute) {
+ this.name = name;
+ this.type = convertType(sourceType);
+ this.matching = matching;
+ this.isAttribute = isAttribute;
+ }
+
+ /** Converts to the right index type from a field datatype */
+ private static Type convertType(DataType fieldType) {
+ FieldValue fval = fieldType.createFieldValue();
+ if (fieldType.equals(DataType.FLOAT)) {
+ return Type.FLOAT;
+ } else if (fieldType.equals(DataType.LONG)) {
+ return Type.INT64;
+ } else if (fieldType.equals(DataType.DOUBLE)) {
+ return Type.DOUBLE;
+ } else if (fieldType.equals(DataType.BYTE)) {
+ return Type.INT8;
+ } else if (fieldType instanceof NumericDataType) {
+ return Type.INT32;
+ } else if (fval instanceof StringFieldValue) {
+ return Type.STRING;
+ } else if (fval instanceof Raw) {
+ return Type.STRING;
+ } else if (fval instanceof PredicateFieldValue) {
+ return Type.UNSEARCHABLESTRING;
+ } else if (fval instanceof TensorFieldValue) {
+ return Type.UNSEARCHABLESTRING;
+ } else if (fieldType instanceof CollectionDataType) {
+ return convertType(((CollectionDataType) fieldType).getNestedType());
+ } else {
+ throw new IllegalArgumentException("Don't know which streaming" +
+ " field type to " + "convert " + fieldType + " to");
+ }
+ }
+
+ public String getName() { return name; }
+
+ public VsmfieldsConfig.Fieldspec.Builder getFieldSpecConfig() {
+ VsmfieldsConfig.Fieldspec.Builder fB = new VsmfieldsConfig.Fieldspec.Builder();
+ String matchingName = matching.getType().getName();
+ if (matching.getType().equals(Matching.Type.TEXT))
+ matchingName = "";
+ if (matching.getType() != Matching.Type.EXACT) {
+ if (matching.isPrefix()) {
+ matchingName = "prefix";
+ } else if (matching.isSubstring()) {
+ matchingName = "substring";
+ } else if (matching.isSuffix()) {
+ matchingName = "suffix";
+ }
+ }
+ if (type != Type.STRING) {
+ matchingName = "";
+ }
+ fB.name(getName())
+ .searchmethod(VsmfieldsConfig.Fieldspec.Searchmethod.Enum.valueOf(type.getSearchMethod()))
+ .arg1(matchingName)
+ .fieldtype(isAttribute
+ ? VsmfieldsConfig.Fieldspec.Fieldtype.ATTRIBUTE
+ : VsmfieldsConfig.Fieldspec.Fieldtype.INDEX);
+ if (matching.maxLength() != null) {
+ fB.maxlength(matching.maxLength());
+ }
+ return fB;
+ }
+
+ public boolean equals(Object o) {
+ if (o.getClass().equals(getClass())) {
+ StreamingField sf = (StreamingField)o;
+ return name.equals(sf.name) &&
+ matching.equals(sf.matching) &&
+ type.equals(sf.type);
+ }
+ return false;
+ }
+
+ }
+
+ private static class StreamingDocumentType {
+ private final String name;
+ private final Map<String, FieldSet> fieldSets = new LinkedHashMap<>();
+ private final Map<String, FieldSet> userFieldSets;
+
+ public StreamingDocumentType(String name, FieldSets fieldSets) {
+ this.name=name;
+ userFieldSets = fieldSets.userFieldSets();
+ }
+
+ public VsmfieldsConfig.Documenttype.Builder getDocTypeConfig() {
+ VsmfieldsConfig.Documenttype.Builder dtB = new VsmfieldsConfig.Documenttype.Builder();
+ dtB.name(name);
+ Map<String, FieldSet> all = new LinkedHashMap<>();
+ all.putAll(fieldSets);
+ all.putAll(userFieldSets);
+ for (Map.Entry<String, FieldSet> e : all.entrySet()) {
+ VsmfieldsConfig.Documenttype.Index.Builder indB = new VsmfieldsConfig.Documenttype.Index.Builder();
+ indB.name(e.getValue().getName());
+ for (String field : e.getValue().getFieldNames()) {
+ indB.field(new VsmfieldsConfig.Documenttype.Index.Field.Builder().name(field));
+ }
+ dtB.index(indB);
+ }
+ return dtB;
+ }
+
+ public String getName() { return name; }
+
+ public void addIndexField(String indexName, String fieldName) {
+ FieldSet fs = fieldSets.get(indexName);
+ if (fs == null) {
+ fs = new FieldSet(indexName);
+ fieldSets.put(indexName, fs);
+ }
+ fs.addFieldName(fieldName);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmSummary.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmSummary.java
new file mode 100644
index 00000000000..aaf376f5cd9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/VsmSummary.java
@@ -0,0 +1,107 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.PositionDataType;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.config.search.vsm.VsmsummaryConfig;
+
+import java.util.*;
+
+/**
+ * Vertical streaming matcher summary specification
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class VsmSummary extends Derived implements VsmsummaryConfig.Producer {
+ private Map<SummaryField, List<String>> summaryMap = new java.util.LinkedHashMap<>(1);
+
+ public VsmSummary(Search search) {
+ derive(search);
+ }
+
+ @Override
+ protected void derive(Search search) {
+ // Use the default class, as it is the superset
+ derive(search, search.getSummary("default"));
+ }
+
+ private void derive(Search search, DocumentSummary documentSummary) {
+ if (documentSummary==null) return;
+ for (SummaryField summaryField : documentSummary.getSummaryFields()) {
+ List<String> from = toStringList(summaryField.sourceIterator());
+
+ if (doMapField(search, summaryField)) {
+ SDField sdField = search.getField(summaryField.getName());
+ if (sdField != null && PositionDataType.INSTANCE.equals(sdField.getDataType())) {
+ summaryMap.put(summaryField, Collections.singletonList(summaryField.getName()));
+ } else {
+ summaryMap.put(summaryField, from);
+ }
+ }
+ }
+ }
+
+ /**
+ * Don't include field in map if sources are the same as the struct sub fields for the SDField.
+ * But do map if not all do summarying.
+ * Don't map if not struct either.
+ * @param summaryField a {@link SummaryField}
+ */
+ private boolean doMapField(Search search, SummaryField summaryField) {
+ SDField sdField = search.getField(summaryField.getName());
+ SDDocumentType document = search.getDocument();
+ if (sdField==null || ((document != null) && (document.getField(summaryField.getName()) == sdField))) {
+ return true;
+ }
+ if (summaryField.getVsmCommand().equals(SummaryField.VsmCommand.FLATTENJUNIPER)) {
+ return true;
+ }
+ if (!sdField.usesStructOrMap()) {
+ return !(sdField.getName().equals(summaryField.getName()));
+ }
+ if (summaryField.getSourceCount()==sdField.getStructFields().size()) {
+ for (SummaryField.Source source : summaryField.getSources()) {
+ if (!sdField.getStructFields().contains(new SDField(search.getDocument(), source.getName(), sdField.getDataType()))) { // equals() uses just name
+ return true;
+ }
+ if (sdField.getStructField(source.getName())!=null && !sdField.getStructField(source.getName()).doesSummarying()) {
+ return true;
+ }
+ }
+ // The sources in the summary field are the same as the sub-fields in the SD field.
+ // All sub fields do summarying.
+ // Don't map.
+ return false;
+ }
+ return true;
+ }
+
+ private List<String> toStringList(Iterator i) {
+ List<String> ret = new ArrayList<>();
+ while (i.hasNext()) {
+ ret.add(i.next().toString());
+ }
+ return ret;
+ }
+
+ public String getDerivedName() {
+ return "vsmsummary";
+ }
+
+ @Override
+ public void getConfig(VsmsummaryConfig.Builder vB) {
+ for (Map.Entry<SummaryField, List<String>> entry : summaryMap.entrySet()) {
+ VsmsummaryConfig.Fieldmap.Builder fmB = new VsmsummaryConfig.Fieldmap.Builder().summary(entry.getKey().getName());
+ for (String field : entry.getValue()) {
+ fmB.document(new VsmsummaryConfig.Fieldmap.Document.Builder().field(field));
+ }
+ fmB.command(VsmsummaryConfig.Fieldmap.Command.Enum.valueOf(entry.getKey().getVsmCommand().toString()));
+ vB.fieldmap(fmB);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/package-info.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/package-info.java
new file mode 100644
index 00000000000..4e63729ea34
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/IndexStructureValidator.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/IndexStructureValidator.java
new file mode 100644
index 00000000000..360b4d0d637
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/IndexStructureValidator.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived.validation;
+
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.searchdefinition.derived.IndexingScript;
+import com.yahoo.vespa.indexinglanguage.ExpressionVisitor;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+
+/**
+ * @author <a href="mailto:mlidal@yahoo-inc.com">Mathias M Lidal</a>
+ */
+public class IndexStructureValidator extends Validator {
+
+ public IndexStructureValidator(DerivedConfiguration config, Search search) {
+ super(config, search);
+ }
+
+ public void validate() {
+ IndexingScript script = config.getIndexingScript();
+ for (Expression exp : script.expressions()) {
+ new OutputVisitor(search.getDocument(), exp).visit(exp);
+ }
+ }
+
+ private static class OutputVisitor extends ExpressionVisitor {
+
+ final SDDocumentType docType;
+ final Expression exp;
+
+ public OutputVisitor(SDDocumentType docType, Expression exp) {
+ this.docType = docType;
+ this.exp = exp;
+ }
+
+ @Override
+ protected void doVisit(Expression exp) {
+ if (!(exp instanceof OutputExpression)) {
+ return;
+ }
+ String fieldName = ((OutputExpression)exp).getFieldName();
+ if (docType.getField(fieldName) != null) {
+ return;
+ }
+ throw new IllegalArgumentException("Indexing expression '" + this.exp + "' refers to field '" +
+ fieldName + "' which does not exist in the index structure.");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validation.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validation.java
new file mode 100644
index 00000000000..9b01881ddd8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validation.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived.validation;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+
+public class Validation {
+
+ public static void validate(DerivedConfiguration config, Search search) {
+ new IndexStructureValidator(config, search).validate();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validator.java b/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validator.java
new file mode 100644
index 00000000000..8c7c5afcb15
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/derived/validation/Validator.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived.validation;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+
+import java.util.logging.Logger;
+
+/**
+ * @author mathiasm
+ */
+public abstract class Validator {
+ protected DerivedConfiguration config;
+ protected Search search;
+
+ protected Validator(DerivedConfiguration config, Search search) {
+ this.config = config;
+ this.search = search;
+ }
+
+ public abstract void validate();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/Attribute.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/Attribute.java
new file mode 100644
index 00000000000..35f6a41f0d8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/Attribute.java
@@ -0,0 +1,320 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import com.yahoo.document.*;
+import com.yahoo.document.datatypes.*;
+import com.yahoo.tensor.TensorType;
+
+import java.io.Serializable;
+import java.util.LinkedHashSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * A search-time document attribute (per-document in-memory value).
+ * This belongs to the field defining the attribute.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public final class Attribute implements Cloneable, Serializable {
+
+ // Remember to change hashCode and equals when you add new fields
+
+ private String name;
+
+ private Type type;
+ private CollectionType collectionType;
+
+ /** True if only the enum information should be read from this attribute
+ * (i.e. the actual values are not relevant, only which documents have the
+ * same values) Used for collapsing and unique.
+ */
+ private boolean removeIfZero = false;
+ private boolean createIfNonExistent = false;
+ private boolean enableBitVectors = false;
+ private boolean enableOnlyBitVector = false;
+
+ private boolean fastSearch = false;
+ private boolean fastAccess = false;
+ private boolean huge = false;
+ private int arity = BooleanIndexDefinition.DEFAULT_ARITY;
+ private long lowerBound = BooleanIndexDefinition.DEFAULT_LOWER_BOUND;
+ private long upperBound = BooleanIndexDefinition.DEFAULT_UPPER_BOUND;
+ private double densePostingListThreshold = BooleanIndexDefinition.DEFAULT_DENSE_POSTING_LIST_THRESHOLD;
+ private Optional<TensorType> tensorType = Optional.empty();
+
+ private boolean isPosition = false;
+ private final Sorting sorting = new Sorting();
+
+ /** The aliases for this attribute */
+ private final Set<String> aliases = new LinkedHashSet<>();
+
+ /**
+ * True if this attribute should be returned during first pass of search.
+ * Null means make the default decision for this kind of attribute
+ */
+ private Boolean prefetch = null;
+
+ /** The attribute type enumeration */
+ public enum Type {
+ BYTE("byte", "INT8"),
+ SHORT("short", "INT16"),
+ INTEGER("integer", "INT32"),
+ LONG("long", "INT64"),
+ FLOAT("float", "FLOAT"),
+ DOUBLE("double", "DOUBLE"),
+ STRING("string", "STRING"),
+ PREDICATE("predicate", "PREDICATE"),
+ TENSOR("tensor", "TENSOR");
+
+ private final String myName; // different from what name() returns.
+ private final String exportAttributeTypeName;
+
+ private Type(String name, String exportAttributeTypeName) {
+ this.myName=name;
+ this.exportAttributeTypeName = exportAttributeTypeName;
+ }
+
+ public String getName() { return myName; }
+ public String getExportAttributeTypeName() { return exportAttributeTypeName; }
+
+ public String toString() {
+ return "type: " + myName;
+ }
+ }
+
+ /** The attribute collection type enumeration */
+ public enum CollectionType {
+
+ SINGLE("SINGLE"),
+ ARRAY("ARRAY"),
+ WEIGHTEDSET ("WEIGHTEDSET");
+
+ private final String name;
+
+ private CollectionType(String name) {
+ this.name=name;
+ }
+
+ public String getName() { return name; }
+
+ public String toString() {
+ return "collectiontype: " + name;
+ }
+ }
+
+ /** Creates an attribute with default settings */
+ public Attribute(String name,DataType fieldType) {
+ this(name,convertDataType(fieldType), convertCollectionType(fieldType));
+ setRemoveIfZero(fieldType instanceof WeightedSetDataType ? ((WeightedSetDataType)fieldType).removeIfZero() : false);
+ setCreateIfNonExistent(fieldType instanceof WeightedSetDataType ? ((WeightedSetDataType)fieldType).createIfNonExistent() : false);
+ }
+
+ public Attribute(String name,Type type, CollectionType collectionType) {
+ this.name=name;
+ setType(type);
+ setCollectionType(collectionType);
+ }
+
+ /**
+ * <p>Returns whether this attribute should be included in the "attributeprefetch" summary
+ * which is returned to the Qrs by prefetchAttributes, used by blending, uniquing etc.
+ *
+ * <p>Single value attributes are prefetched by default if summary is true.
+ * Multi value attributes are not.</p>
+ */
+ public boolean isPrefetch() {
+ if (prefetch!=null) return prefetch.booleanValue();
+
+ if (CollectionType.SINGLE.equals(collectionType)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Returns the prefetch value of this, null if the default is used. */
+ public Boolean getPrefetchValue() { return prefetch; }
+
+ public boolean isRemoveIfZero() { return removeIfZero; }
+ public boolean isCreateIfNonExistent(){ return createIfNonExistent; }
+ public boolean isEnabledBitVectors() { return enableBitVectors; }
+ public boolean isEnabledOnlyBitVector() { return enableOnlyBitVector; }
+ public boolean isFastSearch() { return fastSearch; }
+ public boolean isFastAccess() { return fastAccess; }
+ public boolean isHuge() { return huge; }
+ public boolean isPosition() { return isPosition; }
+
+ public int arity() { return arity; }
+ public long lowerBound() { return lowerBound; }
+ public long upperBound() { return upperBound; }
+ public double densePostingListThreshold() { return densePostingListThreshold; }
+ public Optional<TensorType> tensorType() { return tensorType; }
+
+ public Sorting getSorting() { return sorting; }
+
+ public void setRemoveIfZero(boolean remove) { this.removeIfZero = remove; }
+ public void setCreateIfNonExistent(boolean create) { this.createIfNonExistent = create; }
+
+ /**
+ * Sets whether this should be included in the "attributeprefetch" document summary.
+ * True or false to override default, null to use default
+ */
+ public void setPrefetch(Boolean prefetch) { this.prefetch = prefetch; }
+ public void setEnableBitVectors(boolean enableBitVectors) { this.enableBitVectors = enableBitVectors; }
+ public void setEnableOnlyBitVector(boolean enableOnlyBitVector) { this.enableOnlyBitVector = enableOnlyBitVector; }
+ public void setFastSearch(boolean fastSearch) { this.fastSearch = fastSearch; }
+ public void setHuge(boolean huge) { this.huge = huge; }
+ public void setFastAccess(boolean fastAccess) { this.fastAccess = fastAccess; }
+ public void setPosition(boolean position) { this.isPosition = position; }
+ public void setArity(int arity) { this.arity = arity; }
+ public void setLowerBound(long lowerBound) { this.lowerBound = lowerBound; }
+ public void setUpperBound(long upperBound) { this.upperBound = upperBound; }
+ public void setDensePostingListThreshold(double threshold) { this.densePostingListThreshold = threshold; }
+ public void setTensorType(TensorType tensorType) { this.tensorType = Optional.of(tensorType); }
+
+ public String getName() { return name; }
+ public Type getType() { return type; }
+ public CollectionType getCollectionType() { return collectionType; }
+
+ public void setName(String name) { this.name=name; }
+ private void setType(Type type) { this.type=type; }
+ public void setCollectionType(CollectionType type) { this.collectionType=type; }
+
+ /** Converts to the right attribute type from a field datatype */
+ public static Type convertDataType(DataType fieldType) {
+ FieldValue fval = fieldType.createFieldValue();
+ if (fval instanceof StringFieldValue) {
+ return Type.STRING;
+ } else if (fval instanceof IntegerFieldValue) {
+ return Type.INTEGER;
+ } else if (fval instanceof LongFieldValue) {
+ return Type.LONG;
+ } else if (fval instanceof FloatFieldValue) {
+ return Type.FLOAT;
+ } else if (fval instanceof DoubleFieldValue) {
+ return Type.DOUBLE;
+ } else if (fval instanceof ByteFieldValue) {
+ return Type.BYTE;
+ } else if (fval instanceof Raw) {
+ return Type.BYTE;
+ } else if (fval instanceof PredicateFieldValue) {
+ return Type.PREDICATE;
+ } else if (fval instanceof TensorFieldValue) {
+ return Type.TENSOR;
+ } else if (fieldType instanceof CollectionDataType) {
+ return convertDataType(((CollectionDataType) fieldType).getNestedType());
+ } else {
+ throw new IllegalArgumentException("Don't know which attribute type to " +
+ "convert " + fieldType + " to");
+ }
+ }
+
+ /** Converts to the right attribute type from a field datatype */
+ public static CollectionType convertCollectionType(DataType fieldType) {
+ if (fieldType instanceof ArrayDataType) {
+ return CollectionType.ARRAY;
+ } else if (fieldType instanceof WeightedSetDataType) {
+ return CollectionType.WEIGHTEDSET;
+ } else if (fieldType instanceof PrimitiveDataType) {
+ return CollectionType.SINGLE;
+ } else {
+ throw new IllegalArgumentException("Field " + fieldType + " not supported in convertCollectionType");
+ }
+ }
+
+ /** Converts to the right field type from an attribute type */
+ public static DataType convertAttrType(Type attrType) {
+ if (attrType== Type.STRING) {
+ return DataType.STRING;
+ } else if (attrType== Type.INTEGER) {
+ return DataType.INT;
+ } else if (attrType== Type.LONG) {
+ return DataType.LONG;
+ } else if (attrType== Type.FLOAT) {
+ return DataType.FLOAT;
+ } else if (attrType== Type.DOUBLE) {
+ return DataType.DOUBLE;
+ } else if (attrType == Type.BYTE) {
+ return DataType.BYTE;
+ } else if (attrType == Type.PREDICATE) {
+ return DataType.PREDICATE;
+ } else if (attrType == Type.TENSOR) {
+ return DataType.TENSOR;
+ } else {
+ throw new IllegalArgumentException("Don't know which attribute type to " +
+ "convert " + attrType + " to");
+ }
+ }
+
+ public DataType getDataType() {
+ DataType dataType = Attribute.convertAttrType(type);
+ if (collectionType.equals(Attribute.CollectionType.ARRAY)) {
+ return DataType.getArray(dataType);
+ } else if (collectionType.equals(Attribute.CollectionType.WEIGHTEDSET)) {
+ return DataType.getWeightedSet(dataType, createIfNonExistent, removeIfZero);
+ } else {
+ return dataType;
+ }
+ }
+
+ public int hashCode() {
+ return name.hashCode() +
+ type.hashCode() +
+ collectionType.hashCode() +
+ sorting.hashCode() +
+ (isPrefetch() ? 13 : 0) +
+ (fastSearch ? 17 : 0) +
+ (removeIfZero ? 47 : 0) +
+ (createIfNonExistent ? 53 : 0) +
+ (isPosition ? 61 : 0) +
+ (huge ? 67 : 0) +
+ (enableBitVectors ? 71 : 0) +
+ (enableOnlyBitVector ? 73 : 0) +
+ tensorType.hashCode();
+ }
+
+ public boolean equals(Object object) {
+ if (! (object instanceof Attribute)) return false;
+
+ Attribute other=(Attribute)object;
+ if (!this.name.equals(other.name)) return false;
+ return isCompatible(other);
+ }
+
+ /** Returns whether these attributes describes the same entity, even if they have different names */
+ public boolean isCompatible(Attribute other) {
+ if ( ! this.type.equals(other.type)) return false;
+ if ( ! this.collectionType.equals(other.collectionType)) return false;
+ if (this.isPrefetch() != other.isPrefetch()) return false;
+ if (this.removeIfZero != other.removeIfZero) return false;
+ if (this.createIfNonExistent != other.createIfNonExistent) return false;
+ if (this.enableBitVectors != other.enableBitVectors) return false;
+ if (this.enableOnlyBitVector != other.enableOnlyBitVector) return false;
+ // if (this.noSearch != other.noSearch) return false; No backend consequences so compatible for now
+ if (this.fastSearch != other.fastSearch) return false;
+ if (this.huge != other.huge) return false;
+ if ( ! this.sorting.equals(other.sorting)) return false;
+ if (!this.tensorType.equals(other.tensorType)) return false;
+
+ return true;
+ }
+
+ public @Override Attribute clone() {
+ try {
+ return (Attribute)super.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Programming error");
+ }
+ }
+
+ public String toString() {
+ return "attribute '" + name + "' (" + type + ")";
+ }
+
+ public Set<String> getAliases() {
+ return aliases;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/BooleanIndexDefinition.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/BooleanIndexDefinition.java
new file mode 100644
index 00000000000..fc5494d0f53
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/BooleanIndexDefinition.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+
+/**
+ * Encapsulates values required for native implementation of boolean search.
+ *
+ * @author <a href="mailto:lesters@yahoo-inc.com">Lester Solbakken</a>
+ * @since 5.2
+ */
+public final class BooleanIndexDefinition
+{
+ public static final int DEFAULT_ARITY = 8;
+ public static final long DEFAULT_UPPER_BOUND = Long.MAX_VALUE;
+ public static final long DEFAULT_LOWER_BOUND = Long.MIN_VALUE;
+ public static final double DEFAULT_DENSE_POSTING_LIST_THRESHOLD = 0.4;
+
+ private final OptionalInt arity; // mandatory field value
+ private final OptionalLong lowerBound;
+ private final OptionalLong upperBound;
+ private final OptionalDouble densePostingListThreshold;
+
+ public BooleanIndexDefinition(OptionalInt arity, OptionalLong lowerBound,
+ OptionalLong upperBound, OptionalDouble densePostingListThreshold) {
+ this.arity = arity;
+ this.lowerBound = lowerBound;
+ this.upperBound = upperBound;
+ this.densePostingListThreshold = densePostingListThreshold;
+ }
+
+ public int getArity() {
+ return arity.getAsInt();
+ }
+
+ public boolean hasArity() {
+ return arity.isPresent();
+ }
+
+ public long getLowerBound() {
+ return lowerBound.orElse(DEFAULT_LOWER_BOUND);
+ }
+
+ public boolean hasLowerBound() {
+ return lowerBound.isPresent();
+ }
+
+ public long getUpperBound() {
+ return upperBound.orElse(DEFAULT_UPPER_BOUND);
+ }
+
+ public boolean hasUpperBound() {
+ return upperBound.isPresent();
+ }
+
+ public double getDensePostingListThreshold() {
+ return densePostingListThreshold.orElse(DEFAULT_DENSE_POSTING_LIST_THRESHOLD);
+ }
+
+ public boolean hasDensePostingListThreshold() {
+ return densePostingListThreshold.isPresent();
+ }
+
+ @Override
+ public String toString() {
+ return "BooleanIndexDefinition [arity=" + arity + ", lowerBound="
+ + lowerBound + ", upperBound=" + upperBound + ", densePostingListThreshold="
+ + densePostingListThreshold + "]";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/FieldSet.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/FieldSet.java
new file mode 100644
index 00000000000..040a798d3b3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/FieldSet.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Searchable collection of fields.
+ *
+ * @author balder
+ */
+public class FieldSet {
+
+ private final String name;
+ private final Set<String> queryCommands = new LinkedHashSet<>();
+ private final Set<String> fieldNames = new TreeSet<>();
+ private final Set<SDField> fields = new TreeSet<>();
+ private Matching matching = null;
+
+ public FieldSet(String name) { this.name = name; }
+ public String getName() { return name; }
+ public FieldSet addFieldName(String field) { fieldNames.add(field); return this; }
+ public Set<String> getFieldNames() { return fieldNames; }
+ public Set<SDField> fields() { return fields; }
+
+ public Set<String> queryCommands() {
+ return queryCommands;
+ }
+
+ public void setMatching(Matching matching) {
+ this.matching = matching;
+ }
+
+ public Matching getMatching() {
+ return matching;
+ }
+
+ @Override
+ public String toString() { return "fieldset '" + name + "'"; }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/Matching.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/Matching.java
new file mode 100644
index 00000000000..3c90bcef513
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/Matching.java
@@ -0,0 +1,151 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import java.io.Serializable;
+
+/**
+ * Defines how a field should be matched.
+ * Matching objects can be compared based on their content, but they are <i>not</i> immutable.
+ *
+ * @author bratseth
+ */
+public class Matching implements Cloneable, Serializable {
+
+ public static final Type defaultType = Type.TEXT;
+
+ public static enum Type {
+ TEXT("text"),
+ WORD("word"),
+ EXACT("exact"),
+ GRAM("gram");
+ private String name;
+ Type(String name) { this.name = name; }
+ public String getName() { return name; }
+ }
+
+ /** Which match algorithm is used by this matching setup */
+ public enum Algorithm {
+ NORMAL("normal"),
+ PREFIX("prefix"),
+ SUBSTRING("substring"),
+ SUFFIX("suffix");
+ private String name;
+ Algorithm(String name) { this.name = name; }
+ public String getName() { return name; }
+ }
+
+ private Type type = Type.TEXT;
+
+ /** The basic match algorithm */
+ private Algorithm algorithm = Algorithm.NORMAL;
+
+ private boolean typeUserSet = false;
+
+ private boolean algorithmUserSet = false;
+
+ /** The gram size is the n in n-gram, or -1 if not set. Should only be set with gram matching. */
+ private int gramSize=-1;
+
+ /** Maximum number of characters to consider when searching in this field. Used for limiting resources, especially in streaming search. */
+ private Integer maxLength;
+
+ private String exactMatchTerminator=null;
+
+ /** Creates a matching of type "text" */
+ public Matching() {}
+
+ public Matching(Type type) {
+ this.type = type;
+ }
+
+ public Type getType() { return type; }
+
+ public void setType(Type type) {
+ this.type = type;
+ typeUserSet = true;
+ }
+
+ public Integer maxLength() { return maxLength; }
+ public Matching maxLength(int maxLength) { this.maxLength = maxLength; return this; }
+ public boolean isTypeUserSet() { return typeUserSet; }
+
+ public Algorithm getAlgorithm() { return algorithm; }
+
+ public void setAlgorithm(Algorithm algorithm) {
+ this.algorithm = algorithm;
+ algorithmUserSet = true;
+ }
+
+ public boolean isAlgorithmUserSet() { return algorithmUserSet; }
+
+ public boolean isPrefix() { return algorithm == Algorithm.PREFIX; }
+
+ public boolean isSubstring() { return algorithm == Algorithm.SUBSTRING; }
+
+ public boolean isSuffix() { return algorithm == Algorithm.SUFFIX; }
+
+ /** Returns the gram size, or -1 if not set. Should only be set with gram matching. */
+ public int getGramSize() { return gramSize; }
+
+ public void setGramSize(int gramSize) { this.gramSize=gramSize; }
+
+ /**
+ * Merge data from another matching object
+ */
+ public void merge(Matching m) {
+ if (m.isAlgorithmUserSet()) {
+ this.setAlgorithm(m.getAlgorithm());
+ }
+ if (m.isTypeUserSet()) {
+ this.setType(m.getType());
+ if (m.getType()==Type.GRAM)
+ gramSize=m.gramSize;
+ }
+ if (m.getExactMatchTerminator() != null) {
+ this.setExactMatchTerminator(m.getExactMatchTerminator());
+ }
+ }
+
+ /**
+ * If exact matching is used, this returns the terminator string
+ * which terminates an exact matched sequence in queries. If exact
+ * matching is not used, or no terminator is set, this is null
+ */
+ public String getExactMatchTerminator() { return exactMatchTerminator; }
+
+ /**
+ * Sets the terminator string which terminates an exact matched
+ * sequence in queries (used if type is EXACT).
+ */
+ public void setExactMatchTerminator(String exactMatchTerminator) {
+ this.exactMatchTerminator = exactMatchTerminator;
+ }
+
+ public String toString() {
+ return type + " matching [" + (type==Type.GRAM ? "gram size " + gramSize : "supports " + algorithm) + "], [exact-terminator "+exactMatchTerminator+"]";
+ }
+
+ public Matching clone() {
+ try {
+ return (Matching)super.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Programming error");
+ }
+ }
+
+ public boolean equals(Object o) {
+ if (! (o instanceof Matching)) return false;
+
+ Matching other=(Matching)o;
+ if ( ! other.type.equals(this.type)) return false;
+ if ( ! other.algorithm.equals(this.algorithm)) return false;
+ if ( this.exactMatchTerminator==null && other.exactMatchTerminator!=null) return false;
+ if ( this.exactMatchTerminator!=null && ( ! this.exactMatchTerminator.equals(other.exactMatchTerminator)) )
+ return false;
+ if ( gramSize!=other.gramSize) return false;
+ return true;
+ }
+
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/NormalizeLevel.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/NormalizeLevel.java
new file mode 100644
index 00000000000..945593d550b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/NormalizeLevel.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+/**
+ * class representing the character normalization
+ * we want to do on query and indexed text.
+ * Levels are strict subsets, so doing accent
+ * removal means doing codepoint normalizing
+ * and case normalizing also.
+ */
+// TODO: Missing author
+public class NormalizeLevel {
+
+ /**
+ * The current levels are as follows:
+ * NONE: no changes to input text
+ * CODEPOINT: convert text into Unicode
+ * Normalization Form Compatibility Composition
+ * LOWERCASE: also convert text into lowercase letters
+ * ACCENT: do both above and remove accents on characters
+ */
+ public enum Level {
+ NONE, CODEPOINT, LOWERCASE, ACCENT
+ }
+
+ private boolean userSpecified = false;
+ private Level level = Level.ACCENT;
+
+ /**
+ * Returns whether accents should be removed from text
+ */
+ public boolean doRemoveAccents() { return level == Level.ACCENT; }
+
+ /**
+ * Construct a default (full) normalizelevel,
+ */
+ public NormalizeLevel() {}
+
+ /**
+ * Construct for a specific level, possibly user specified
+ *
+ * @param level which level to use
+ * @param fromUser whether this was specified by the user
+ */
+ public NormalizeLevel(Level level, boolean fromUser) {
+ this.level = level;
+ this.userSpecified = fromUser;
+ }
+
+ /**
+ * Change the current level to CODEPOINT as inferred
+ * by other features' needs. If the current level
+ * was user specified it will not change; also this
+ * will not increase the level.
+ */
+ public void inferCodepoint() {
+ if (userSpecified) {
+ // ignore inferred changes if user specified something
+ return;
+ }
+ // do not increase level
+ if (level != Level.NONE) level = Level.CODEPOINT;
+ }
+
+ /**
+ * Change the current level to LOWERCASE as inferred
+ * by other features' needs. If the current level
+ * was user specified it will not change; also this
+ * will not increase the level.
+ */
+ public void inferLowercase() {
+ if (userSpecified) {
+ // ignore inferred changes if user specified something
+ return;
+ }
+ // do not increase level
+ if (level == Level.NONE) return;
+ if (level == Level.CODEPOINT) return;
+
+ level = Level.LOWERCASE;
+ }
+
+ public Level getLevel() {
+ return level;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/RankType.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/RankType.java
new file mode 100644
index 00000000000..f2b0b6a9164
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/RankType.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * The rank type of a field. For now this is just a container of a string name.
+ * This class is immutable.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public enum RankType {
+
+ /** *implicit* default: No type has been set. */
+ DEFAULT,
+
+ // Rank types which can be set explicitly. These are defined for Vespa in NativeRankTypeDefinitionSet
+ IDENTITY, ABOUT, TAGS, EMPTY;
+
+ @Override
+ public String toString() {
+ return "rank type " + name().toLowerCase();
+ }
+
+ /**
+ * Returns the rank type from a string, regardless of its case.
+ *
+ * @param rankTypeName a rank type name in any casing
+ * @return the rank type found
+ * @throws IllegalArgumentException if not found
+ */
+ public static RankType fromString(String rankTypeName) {
+ try {
+ return RankType.valueOf(rankTypeName.toUpperCase());
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Unknown rank type '" + rankTypeName + "'. Supported rank types are " +
+ "'identity', 'about', 'tags' and 'empty'.");
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/Ranking.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/Ranking.java
new file mode 100644
index 00000000000..9e0bda7fd5a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/Ranking.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import java.io.Serializable;
+
+/**
+ * The rank settings given in a rank clause in the search definition.
+ *
+ * @author <a href="mailto:vehardh@yahoo-inc.com">Vegard Havdal</a>
+ */
+public class Ranking implements Cloneable, Serializable {
+
+ private boolean literal = false;
+ private boolean filter = false;
+ private boolean normal = false;
+
+ /**
+ * <p>Returns whether literal (non-stemmed, non-normalized) forms of the words should
+ * be indexed in a separate index which is searched by a automatically added rank term
+ * during searches.</p>
+ *
+ * <p>Default is false.</p>
+ */
+ public boolean isLiteral() { return literal; }
+
+ public void setLiteral(boolean literal) { this.literal = literal; }
+
+ /**
+ * <p>Returns whether this is a filter. Filters will only tell if they are matched or not,
+ * no detailed relevance information will be available about the match.</p>
+ *
+ * <p>Matching a filter is much cheaper for the search engine than matching a regular field.</p>
+ *
+ * <p>Default is false.</p>
+ */
+ public boolean isFilter() { return filter && !normal; }
+
+ public void setFilter(boolean filter) { this.filter = filter; }
+
+ /** Whether user has explicitly requested normal (non-filter) behavior */
+ public boolean isNormal() { return normal; }
+ public void setNormal(boolean n) { this.normal = n; }
+
+ /** Returns true if the given rank settings are the same */
+ public @Override boolean equals(Object o) {
+ if ( ! (o instanceof Ranking)) return false;
+
+ Ranking other=(Ranking)o;
+ if (this.filter != other.filter) return false;
+ if (this.literal != other.literal) return false;
+ if (this.normal != other.normal) return false;
+ return true;
+ }
+
+ public @Override String toString() {
+ return "rank settings [filter: " + filter + ", literal: " + literal + ", normal: "+normal+"]";
+ }
+
+ public @Override Ranking clone() {
+ try {
+ return (Ranking)super.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Programming error",e);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/SDDocumentType.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/SDDocumentType.java
new file mode 100644
index 00000000000..6292dc9ef72
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/SDDocumentType.java
@@ -0,0 +1,316 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import com.yahoo.document.*;
+import com.yahoo.document.annotation.AnnotationType;
+import com.yahoo.document.annotation.AnnotationTypeRegistry;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.documentmodel.VespaDocumentType;
+import com.yahoo.searchdefinition.FieldSets;
+import com.yahoo.searchdefinition.Search;
+
+import java.io.Serializable;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ <p>A document definition is a list of fields. Documents may inherit other documents,
+ implicitly acquiring their fields as it's own. If a document is not set to inherit
+ any document, it will always inherit the document "document.0".</p>
+
+ @author <a href="thomasg@yahoo-inc.com">Thomas Gundersen</a>
+ @author <a href="bratseth@yahoo-inc.com">Jon S Bratseth</a>
+*/
+public class SDDocumentType implements Cloneable, Serializable {
+ public static final SDDocumentType VESPA_DOCUMENT;
+ private Map<DataTypeName, SDDocumentType> inheritedTypes = new HashMap<>();
+ private Map<NewDocumentType.Name, SDDocumentType> ownedTypes = new HashMap<>();
+ private AnnotationTypeRegistry annotationTypes = new AnnotationTypeRegistry();
+ private DocumentType docType;
+ private DataType structType;
+ // The field sets here are set from the processing step in SD, to ensure that the full Search and this SDDocumentType is built first.
+ private FieldSets fieldSets;
+
+ static {
+ VESPA_DOCUMENT = new SDDocumentType(VespaDocumentType.INSTANCE.getFullName().getName());
+ VESPA_DOCUMENT.addType(createSDDocumentType(PositionDataType.INSTANCE));
+ }
+
+ public SDDocumentType clone() throws CloneNotSupportedException {
+ SDDocumentType type = (SDDocumentType) super.clone();
+ type.docType = docType.clone();
+ type.inheritedTypes.putAll(inheritedTypes);
+ type.structType = structType;
+ return type;
+ }
+
+ /**
+ * For adding structs defined in document scope
+ *
+ * @param dt The struct to add.
+ * @return self, for chaining
+ */
+ public SDDocumentType addType(SDDocumentType dt) {
+ NewDocumentType.Name name = new NewDocumentType.Name(dt.getName());
+ if (getType(name) != null) {
+ throw new IllegalArgumentException("Data type '" + name.toString() + "' has already been used.");
+ }
+ if (name.getName() == docType.getName()) {
+ throw new IllegalArgumentException("Data type '" + name.toString() + "' can not have same name as its defining document.");
+ }
+ ownedTypes.put(name, dt);
+ return this;
+ }
+ public final SDDocumentType getOwnedType(String name) {
+ return getOwnedType(new NewDocumentType.Name(name));
+ }
+ public SDDocumentType getOwnedType(DataTypeName name) {
+ return getOwnedType(name.getName());
+ }
+
+ public SDDocumentType getOwnedType(NewDocumentType.Name name) {
+ return ownedTypes.get(name);
+ }
+
+ public final SDDocumentType getType(String name) {
+ return getType(new NewDocumentType.Name(name));
+ }
+
+ public SDDocumentType getType(NewDocumentType.Name name) {
+ SDDocumentType type = ownedTypes.get(name);
+ if (type == null) {
+ for (SDDocumentType inherited : inheritedTypes.values()) {
+ type = inherited.getType(name);
+ if (type != null) {
+ return type;
+ }
+ }
+ }
+ return type;
+ }
+
+ public SDDocumentType addAnnotation(AnnotationType annotation) {
+ annotationTypes.register(annotation);
+ return this;
+ }
+
+ /**
+ * Access to all owned datatypes.
+ * @return all types
+ */
+ public Collection<SDDocumentType> getTypes() { return ownedTypes.values(); }
+ public Collection<AnnotationType> getAnnotations() { return annotationTypes.getTypes().values(); }
+ public AnnotationType findAnnotation(String name) { return annotationTypes.getType(name); }
+
+ public Collection<SDDocumentType> getAllTypes() {
+ Collection<SDDocumentType> list = new ArrayList<>();
+ list.addAll(getTypes());
+ for (SDDocumentType inherited : inheritedTypes.values()) {
+ list.addAll(inherited.getAllTypes());
+ }
+ return list;
+ }
+
+ /**
+ * Creates a new document type.
+ * The document type id will be generated as a hash from the document type name.
+ *
+ * @param name The name of the new document type
+ */
+ public SDDocumentType(String name) {
+ this(name,null);
+ }
+
+ public SDDocumentType(DataTypeName name) {
+ this(name.getName());
+ }
+
+ /**
+ * Creates a new document type.
+ * The document type id will be generated as a hash from the document type name.
+ *
+ * @param name The name of the new document type
+ * @param search check for type ID collisions in this search definition
+ */
+ public SDDocumentType(String name, Search search) {
+ docType = new DocumentType(name);
+ docType.getHeaderType().setCompressionConfig(new CompressionConfig());
+ docType.getBodyType().setCompressionConfig(new CompressionConfig());
+ validateId(search);
+ inherit(VESPA_DOCUMENT);
+ }
+
+ public boolean isStruct() { return getStruct() != null; }
+ public DataType getStruct() { return structType; }
+ public SDDocumentType setStruct(DataType structType) {
+ if (structType != null) {
+ this.structType = structType;
+ inheritedTypes.clear();
+ } else {
+ if (docType.getHeaderType() != null) {
+ this.structType = docType.getHeaderType();
+ inheritedTypes.clear();
+ } else {
+ throw new IllegalArgumentException("You can not set a null struct");
+ }
+ }
+ return this;
+ }
+
+ public String getName() { return docType.getName(); }
+ public DataTypeName getDocumentName() { return docType.getDataTypeName(); }
+ public DocumentType getDocumentType() { return docType; }
+
+ public void inherit(DataTypeName name) {
+ if ( ! inheritedTypes.containsKey(name)) {
+ inheritedTypes.put(name, new TemporarySDDocumentType(name));
+ }
+ }
+
+ public void inherit(SDDocumentType type) {
+ if (type != null) {
+ if (!inheritedTypes.containsKey(type.getDocumentName()) || (inheritedTypes.get(type.getDocumentName()) instanceof TemporarySDDocumentType)) {
+ inheritedTypes.put(type.getDocumentName(), type);
+ }
+ }
+ }
+
+ public Collection<SDDocumentType> getInheritedTypes() { return inheritedTypes.values(); }
+
+ protected void validateId(Search search) {
+ if (search == null) return;
+ if (search.getDocument(getName()) == null) return;
+ SDDocumentType doc = search.getDocument();
+ throw new IllegalArgumentException("Failed creating document type '" + getName() + "', " +
+ "document type '" + doc.getName() + "' already uses ID '" + doc.getName() + "'");
+ }
+
+ public void setFieldId(SDField field, int id) {
+ field.setId(id, docType);
+ }
+
+ /** Override getField, as it may need to ask inherited types that isn't registered in document type.
+ * @param name Name of field to get.
+ * @return The field found.
+ */
+ public Field getField(String name) {
+ if (name.contains(".")) {
+ String superFieldName = name.substring(0,name.indexOf("."));
+ String subFieldName = name.substring(name.indexOf(".")+1);
+ Field f = docType.getField(superFieldName);
+ if (f != null) {
+ if (f instanceof SDField) {
+ SDField superField = (SDField)f;
+ return superField.getStructField(subFieldName);
+ } else {
+ throw new IllegalArgumentException("Field "+f.getName()+" is not SDField");
+ }
+ }
+ }
+ Field f = docType.getField(name);
+ if (f == null) {
+ for(SDDocumentType parent : inheritedTypes.values()) {
+ f = parent.getField(name);
+ if (f != null) return f;
+ }
+ }
+ return f;
+ }
+
+ public void addField(Field field) {
+ verifyInheritance(field);
+ for (Iterator<Field> i = docType.fieldIteratorThisTypeOnly(); i.hasNext(); ) {
+ if (field.getName().equalsIgnoreCase((i.next()).getName())) {
+ throw new IllegalArgumentException("Duplicate (case insensitively) " + field + " in " + this);
+ }
+ }
+ docType.addField(field);
+ }
+
+ /**
+ * This is SD parse-time inheritance check.
+ *
+ * @param field The field being verified.
+ */
+ private void verifyInheritance(Field field) {
+ for (SDDocumentType parent : inheritedTypes.values()) {
+ for (Field pField : parent.fieldSet()) {
+ if (pField.getName().equals(field.getName())) {
+ if (!pField.getDataType().equals(field.getDataType())) {
+ throw new IllegalArgumentException("For search '"+getName()+"', field '"+field.getName()+"': " +
+ "datatype can't be different from that of same field in supertype '"+parent.getName()+"'.");
+ }
+ }
+ }
+ }
+ }
+
+ public SDField addField(String string, DataType dataType) {
+ SDField field = new SDField(this, string, dataType);
+ addField(field);
+ return field;
+ }
+
+ public Field addField(String string, DataType dataType, boolean header, int code) {
+ SDField field = new SDField(this, string, code, dataType, header);
+ addField(field);
+ return field;
+ }
+
+ private Map<String, Field> fieldsInherited() {
+ Map<String, Field> map = new LinkedHashMap<>();
+ for (SDDocumentType parent : inheritedTypes.values()) {
+ for (Field field : parent.fieldSet()) {
+ map.put(field.getName(), field);
+ }
+ }
+ return map;
+ }
+
+ public Set<Field> fieldSet() {
+ Map<String, Field> map = fieldsInherited();
+ Iterator<Field> it = docType.fieldIteratorThisTypeOnly();
+ while (it.hasNext()) {
+ Field field = it.next();
+ map.put(field.getName(), field);
+ }
+ return new LinkedHashSet<>(map.values());
+ }
+
+ public Iterator<Field> fieldIterator() {
+ return fieldSet().iterator();
+ }
+
+ public int getFieldCount() {
+ return docType.getFieldCount();
+ }
+
+ public String toString() {
+ return "SD document type '" + docType.getName() + "'";
+ }
+
+ private static SDDocumentType createSDDocumentType(StructDataType structType) {
+ SDDocumentType docType = new SDDocumentType(structType.getName());
+ for (Field field : structType.getFields()) {
+ docType.addField(new SDField(docType, field.getName(), field.getDataType()));
+ }
+ docType.setStruct(structType);
+ return docType;
+ }
+
+ /**
+ * The field sets defined for this type and its {@link Search}
+ * @return fieldsets
+ */
+ public FieldSets getFieldSets() {
+ return fieldSets;
+ }
+
+ /**
+ * Sets the field sets for this
+ * @param fieldSets field sets to set for this object
+ */
+ public void setFieldSets(FieldSets fieldSets) {
+ this.fieldSets = fieldSets;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/SDField.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/SDField.java
new file mode 100644
index 00000000000..b379abd63ec
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/SDField.java
@@ -0,0 +1,770 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import com.yahoo.document.*;
+import com.yahoo.language.Linguistics;
+import com.yahoo.language.simple.SimpleLinguistics;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.fieldoperation.FieldOperation;
+import com.yahoo.searchdefinition.fieldoperation.FieldOperationContainer;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.indexinglanguage.ExpressionSearcher;
+import com.yahoo.vespa.indexinglanguage.ExpressionVisitor;
+import com.yahoo.vespa.indexinglanguage.ScriptParserContext;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.indexinglanguage.parser.IndexingInput;
+import com.yahoo.vespa.indexinglanguage.parser.ParseException;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * The field class represents a document field. It is used in
+ * the Document class to get and set fields. Each SDField has
+ * a name, a numeric ID, a data type, and a boolean that says whether it's
+ * a header field. The numeric ID is used when the fields are stored
+ * in serialized form.
+ *
+ * @author bratseth
+ */
+public class SDField extends Field implements TypedKey, FieldOperationContainer, Serializable {
+
+ /** Use this field for modifying index-structure, even if it doesn't
+ have any indexing code */
+ private boolean indexStructureField = false;
+
+ /** The indexing statements to be applied to this value during indexing */
+ private ScriptExpression indexingScript = new ScriptExpression();
+
+ /** The default rank type for indices of this field */
+ private RankType rankType = RankType.DEFAULT;
+
+ /** Rank settings in a "rank" block for the field. */
+ private Ranking ranking = new Ranking();
+
+ /**
+ * The literal boost of this field. This boost is added to a rank score
+ * when a query term matched as query term exactly (unnormalized and unstemmed).
+ * Non-positive boosts causes no boosting, 0 allows boosts
+ * to be specified in other rank profiles, while negative values
+ * turns the capability off.
+ */
+ private int literalBoost=-1;
+
+ /** The weight of this field. This is a percentage,
+ * so 100 is default to provide the identity transform. */
+ private int weight=100;
+
+ /**
+ * Indicates what kind of matching should be done on this field
+ */
+ private Matching matching=new Matching();
+
+ /** Attribute settings, or null if there are none */
+ private Map<String, Attribute> attributes = new TreeMap<>();
+
+ /**
+ * The stemming setting of this field, or null to use the default.
+ * Default is determined by the owning search definition.
+ */
+ private Stemming stemming=null;
+
+ /** How content of this field should be accent normalized etc. */
+ private NormalizeLevel normalizing = new NormalizeLevel();
+
+ /** Extra query commands of this field */
+ private List<String> queryCommands=new java.util.ArrayList<>(0);
+
+ /** Summary fields defined in this field */
+ private Map<String,SummaryField> summaryFields = new java.util.LinkedHashMap<>(0);
+
+ /** The explicitly index settings on this field */
+ private Map<String, Index> indices=new java.util.LinkedHashMap<>();
+
+ /** True if body or header is set explicitly for this field */
+ private boolean headerOrBodyDefined = false;
+
+ private boolean idOverride = false;
+
+ /** Struct fields defined in this field */
+ private Map<String,SDField> structFields = new java.util.LinkedHashMap<>(0);
+
+ /** The document that this field was declared in, or null*/
+ protected SDDocumentType ownerDocType = null;
+
+ /** The aliases declared for this field. May pertain to indexes or attributes */
+ private Map<String, String> aliasToName = new HashMap<>();
+
+ /** Pending operations that must be applied after parsing, due to use of not-yet-defined structs. */
+ private List<FieldOperation> pendingOperations = new LinkedList<>();
+
+ private boolean isExtraField = false;
+
+ /**
+ Creates a new field. This method is only used to create reserved fields
+ @param name The name of the field
+ @param dataType The datatype of the field
+ @param isHeader Whether this is a "header" field or a "content" field
+ (true = "header").
+ */
+ protected SDField(SDDocumentType repo, String name, int id, DataType dataType, boolean isHeader, boolean populate) {
+ super(name, id, dataType, isHeader);
+ populate(populate, repo, name, dataType, isHeader);
+ }
+
+ public SDField(SDDocumentType repo, String name, int id, DataType dataType, boolean isHeader) {
+ this(repo, name, id, dataType, isHeader, true);
+ }
+
+ /**
+ Creates a new field.
+
+ @param name The name of the field
+ @param dataType The datatype of the field
+ @param isHeader Whether this is a "header" field or a "content" field
+ (true = "header").
+ */
+ public SDField(SDDocumentType repo, String name, DataType dataType, boolean isHeader, boolean populate) {
+ super(name,dataType,isHeader);
+ populate(populate, repo, name, dataType, isHeader);
+ }
+
+ private void populate(boolean populate, SDDocumentType repo, String name, DataType dataType, boolean isHeader) {
+ populate(populate,repo, name, dataType, isHeader, null, 0);
+ }
+
+ private void populate(boolean populate, SDDocumentType repo, String name, DataType dataType, boolean isHeader, Matching fieldMatching, int recursion) {
+ if (populate || (dataType instanceof MapDataType)) {
+ populateWithStructFields(repo, name, dataType, isHeader, recursion);
+ populateWithStructMatching(repo, name, dataType, fieldMatching);
+ }
+ }
+
+ public SDField(String name, DataType dataType, boolean isHeader) {
+ this(null, name, dataType, isHeader, true);
+ }
+
+ /**
+ Creates a new field.
+
+ @param name The name of the field
+ @param dataType The datatype of the field
+ @param isHeader Whether this is a "header" field or a "content" field
+ (true = "header").
+ @param owner the owning document (used to check for id collisions)
+ */
+ protected SDField(SDDocumentType repo, String name, DataType dataType, boolean isHeader, SDDocumentType owner, boolean populate) {
+ super(name, dataType, isHeader, owner == null ? null : owner.getDocumentType());
+ this.ownerDocType=owner;
+ populate(populate, repo, name, dataType, isHeader);
+ }
+
+ /**
+ Creates a new field.
+
+ @param name The name of the field
+ @param dataType The datatype of the field
+ @param isHeader Whether this is a "header" field or a "content" field
+ (true = "header").
+ @param owner The owning document (used to check for id collisions)
+ @param fieldMatching The matching object to set for the field
+ */
+ protected SDField(SDDocumentType repo, String name, DataType dataType, boolean isHeader, SDDocumentType owner, Matching fieldMatching, boolean populate, int recursion) {
+ super(name, dataType, isHeader, owner == null ? null : owner.getDocumentType());
+ this.ownerDocType=owner;
+ if (fieldMatching != null) {
+ this.setMatching(fieldMatching);
+ }
+ populate(populate, repo, name, dataType, isHeader, fieldMatching, recursion);
+ }
+
+ /**
+ Constructor for <b>header</b> fields
+
+ @param name The name of the field
+ @param dataType The datatype of the field
+ */
+ public SDField(SDDocumentType repo, String name, DataType dataType) {
+ this(repo, name,dataType,true, true);
+ }
+ public SDField(String name, DataType dataType) {
+ this(null, name,dataType);
+ }
+
+ public void setIsExtraField(boolean isExtra) {
+ isExtraField = isExtra;
+ }
+
+ public boolean isExtraField() {
+ return isExtraField;
+ }
+
+ public boolean doesAttributing() {
+ return containsExpression(AttributeExpression.class);
+ }
+
+ public boolean doesIndexing() {
+ return containsExpression(IndexExpression.class);
+ }
+
+ public boolean doesSummarying() {
+ if (usesStruct()) {
+ for (SDField structField : getStructFields()) {
+ if (structField.doesSummarying()) {
+ return true;
+ }
+ }
+ }
+ return containsExpression(SummaryExpression.class);
+ }
+
+ public boolean doesLowerCasing() {
+ return containsExpression(LowerCaseExpression.class);
+ }
+
+ public <T extends Expression> boolean containsExpression(Class<T> searchFor) {
+ return findExpression(searchFor) != null;
+ }
+
+ public <T extends Expression> T findExpression(Class<T> searchFor) {
+ return new ExpressionSearcher<>(searchFor).searchIn(indexingScript);
+ }
+
+ public void addSummaryFieldSources(SummaryField summaryField) {
+ if (usesStruct()) {
+ /*
+ * How this works for structs: When at least one sub-field in a struct is to
+ * be used for summary, that whole struct field is included in summary.cfg. Then,
+ * vsmsummary.cfg specifies the sub-fields used for each struct field.
+ * So we recurse into each struct, adding the destination classes set for each sub-field
+ * to the main summary-field for the struct field.
+ */
+ for (SDField structField : getStructFields()) {
+ for (SummaryField sumF : structField.getSummaryFields()) {
+ for (String dest : sumF.getDestinations()) {
+ summaryField.addDestination(dest);
+ }
+ }
+ structField.addSummaryFieldSources(summaryField);
+ }
+ } else {
+ if (doesSummarying()) {
+ summaryField.addSource(getName());
+ }
+ }
+ }
+
+ public void populateWithStructFields(SDDocumentType sdoc, String name, DataType dataType, boolean isHeader, int recursion) {
+ DataType dt = getFirstStructOrMapRecursive();
+ if (dt == null) {
+ return;
+ }
+ if (dataType instanceof MapDataType) {
+ MapDataType mdt = (MapDataType) dataType;
+
+ SDField keyField = new SDField(sdoc, name.concat(".key"), mdt.getKeyType(),
+ isHeader, getOwnerDocType(), new Matching(), true, recursion + 1);
+ structFields.put("key", keyField);
+
+ SDField valueField = new SDField(sdoc, name.concat(".value"), mdt.getValueType(),
+ isHeader, getOwnerDocType(), new Matching(), true, recursion + 1);
+ structFields.put("value", valueField);
+ } else {
+ if (recursion >= 10) {
+ return;
+ }
+ if (dataType instanceof CollectionDataType) {
+ dataType = ((CollectionDataType)dataType).getNestedType();
+ }
+ SDDocumentType subType = sdoc != null ? sdoc.getType(dataType.getName()) : null;
+ if (subType == null) {
+ throw new IllegalArgumentException("Could not find struct '" + dataType.getName() + "'.");
+ }
+ for (Field field : subType.fieldSet()) {
+ SDField subField = new SDField(sdoc, name.concat(".").concat(field.getName()), field.getDataType(),
+ isHeader, subType, new Matching(), true, recursion + 1);
+ structFields.put(field.getName(), subField);
+ }
+ }
+ }
+
+ public void populateWithStructMatching(SDDocumentType sdoc, String name, DataType dataType,
+ Matching superFieldMatching) {
+ DataType dt = getFirstStructOrMapRecursive();
+ if (dt != null) {
+ if (dataType instanceof MapDataType) {
+ MapDataType mdt = (MapDataType) dataType;
+
+ Matching keyFieldMatching = new Matching();
+ if (superFieldMatching != null) {
+ keyFieldMatching.merge(superFieldMatching);
+ }
+ SDField keyField = structFields.get(name.concat(".key"));
+ if (keyField != null) {
+ keyField.populateWithStructMatching(sdoc, name.concat(".key"), mdt.getKeyType(), keyFieldMatching);
+ keyField.setMatching(keyFieldMatching);
+ }
+
+ Matching valueFieldMatching = new Matching();
+ if (superFieldMatching != null) {
+ valueFieldMatching.merge(superFieldMatching);
+ }
+ SDField valueField = structFields.get(name.concat(".value"));
+ if (valueField != null) {
+ valueField.populateWithStructMatching(sdoc, name.concat(".value"), mdt.getValueType(),
+ valueFieldMatching);
+ valueField.setMatching(valueFieldMatching);
+ }
+
+ } else {
+
+ if (dataType instanceof CollectionDataType) {
+ dataType = ((CollectionDataType)dataType).getNestedType();
+ }
+ SDDocumentType subType = sdoc != null ? sdoc.getType(dataType.getName()) : null;
+ if (subType != null) {
+ for (Field f : subType.fieldSet()) {
+ if (f instanceof SDField) {
+ SDField field = (SDField)f;
+ Matching subFieldMatching = new Matching();
+ if (superFieldMatching != null) {
+ subFieldMatching.merge(superFieldMatching);
+ }
+ subFieldMatching.merge(field.getMatching());
+ SDField subField = structFields.get(field.getName());
+ if (subField != null) {
+ subField.populateWithStructMatching(sdoc, name.concat(".").concat(field.getName()), field.getDataType(),
+ subFieldMatching);
+ subField.setMatching(subFieldMatching);
+ }
+ } else {
+ throw new IllegalArgumentException("Field in struct is not SDField " + f.getName());
+ }
+ }
+ } else {
+ throw new IllegalArgumentException("Could not find struct " + dataType.getName());
+ }
+ }
+ }
+ }
+
+ public void addOperation(FieldOperation op) {
+ pendingOperations.add(op);
+ }
+
+ public void applyOperations(SDField field) {
+ if (pendingOperations.isEmpty()) {
+ return;
+ }
+ ListIterator<FieldOperation> ops = pendingOperations.listIterator();
+ while (ops.hasNext()) {
+ FieldOperation op = ops.next();
+ ops.remove();
+ op.apply(field);
+ }
+ }
+
+ public void applyOperations() {
+ applyOperations(this);
+ }
+
+ public void setId(int fieldId, DocumentType owner) {
+ super.setId(fieldId, owner);
+ idOverride = true;
+ }
+
+ public StructDataType getFirstStructRecursive() {
+ DataType dataType = getDataType();
+ while (true) { // Currently no nesting of collections
+ if (dataType instanceof CollectionDataType) {
+ dataType = ((CollectionDataType)dataType).getNestedType();
+ } else if (dataType instanceof MapDataType) {
+ dataType = ((MapDataType)dataType).getValueType();
+ } else {
+ break;
+ }
+ }
+ return (dataType instanceof StructDataType) ? (StructDataType)dataType : null;
+ }
+
+ public DataType getFirstStructOrMapRecursive() {
+ DataType dataType = getDataType();
+ while (dataType instanceof CollectionDataType) { // Currently no nesting of collections
+ dataType = ((CollectionDataType)dataType).getNestedType();
+ }
+ return (dataType instanceof StructDataType || dataType instanceof MapDataType) ? dataType : null;
+ }
+
+ public boolean usesStruct() {
+ DataType dt = getFirstStructRecursive();
+ return (dt != null);
+ }
+
+ public boolean usesStructOrMap() {
+ DataType dt = getFirstStructOrMapRecursive();
+ return (dt != null);
+ }
+
+ /** Parse an indexing expression which will use the simple linguistics implementatino suitable for testing */
+ public void parseIndexingScript(String script) {
+ parseIndexingScript(script, new SimpleLinguistics());
+ }
+
+ public void parseIndexingScript(String script, Linguistics linguistics) {
+ try {
+ ScriptParserContext config = new ScriptParserContext(linguistics);
+ config.setInputStream(new IndexingInput(script));
+ setIndexingScript(ScriptExpression.newInstance(config));
+ } catch (ParseException e) {
+ throw new RuntimeException("Failed to parser script '" + script + "'.", e);
+ }
+ }
+
+ /** Sets the indexing script of this, or null to not use a script */
+ public void setIndexingScript(ScriptExpression exp) {
+ if (exp == null) {
+ exp = new ScriptExpression();
+ }
+ indexingScript = exp;
+ if (indexingScript.isEmpty()) {
+ return; // TODO: This causes empty expressions not to be propagate to struct fields!! BAD BAD BAD!!
+ }
+ if (!usesStructOrMap()) {
+ new ExpressionVisitor() {
+
+ @Override
+ protected void doVisit(Expression exp) {
+ if (!(exp instanceof AttributeExpression)) {
+ return;
+ }
+ String fieldName = ((AttributeExpression)exp).getFieldName();
+ if (fieldName == null) {
+ fieldName = getName();
+ }
+ Attribute attribute = attributes.get(fieldName);
+ if (attribute == null) {
+ addAttribute(new Attribute(fieldName, getDataType()));
+ }
+ }
+ }.visit(indexingScript);
+ }
+ for (SDField structField : getStructFields()) {
+ structField.setIndexingScript(exp);
+ }
+ }
+
+ public ScriptExpression getIndexingScript() {
+ return indexingScript;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setDataType(DataType type) {
+ if (type.equals(DataType.URI)) { // Different defaults, naturally
+ normalizing.inferLowercase();
+ stemming=Stemming.NONE;
+ }
+ this.dataType = type;
+ if (!idOverride) {
+ this.fieldId = calculateIdV7(null);
+ }
+ }
+
+ public boolean isIndexStructureField() {
+ return indexStructureField;
+ }
+
+ public void setIndexStructureField(boolean indexStructureField) {
+ this.indexStructureField = indexStructureField;
+ }
+
+ /**
+ * Returns an iterator of the index names this should index to
+ * (whether set explicitly or not)
+ */
+ public Iterator<String> getFieldNameAsIterator() { // TODO: Replace usage by getName
+ return Collections.singletonList(getName()).iterator();
+ }
+
+ /** Returns 1 if this is indexed, 0 if it is not indexed */ // TODO: Replace by a boolean method, or something, see hasIndex
+ public int getIndexToCount() {
+ if (getIndexingScript() == null) return 0;
+ if (!doesIndexing()) return 0;
+
+ return 1;
+ }
+
+ /** Sets the literal boost of this field */
+ public void setLiteralBoost(int literalBoost) { this.literalBoost=literalBoost; }
+
+ /**
+ * Returns the literal boost of this field. This boost is added to a literal score
+ * when a query term matched as query term exactly (unnormalized and unstemmed).
+ * Default is non-positive.
+ */
+ public int getLiteralBoost() { return literalBoost; }
+
+ /** Sets the weight of this field */
+ public void setWeight(int weight) { this.weight=weight; }
+
+ /** Returns the weight of this field, or 0 if nothing is set */
+ public int getWeight() { return weight; }
+
+ /**
+ * Returns what kind of matching type should be applied.
+ */
+ public Matching getMatching() { return matching; }
+
+ /**
+ * Sets what kind of matching type should be applied.
+ * (Token matching is default, PREFIX, SUBSTRING, SUFFIX are alternatives)
+ */
+ public void setMatching(Matching matching) { this.matching=matching; }
+
+ /**
+ * Set the matching type for this field and all subfields.
+ */
+ // TODO: When this is not the same as getMatching().setthis we have a potential for inconsistency. Find the right
+ // Matching object for struct fields as lookup time instead.
+ public void setMatchingType(Matching.Type type) {
+ this.getMatching().setType(type);
+ for (SDField structField : getStructFields()) {
+ structField.setMatchingType(type);
+ }
+ }
+
+ /**
+ * Set matching algorithm for this field and all subfields.
+ */
+ // TODO: When this is not the same as getMatching().setthis we have a potential for inconsistency. Find the right
+ // Matching object for struct fields as lookup time instead.
+ public void setMatchingAlgorithm(Matching.Algorithm algorithm) {
+ this.getMatching().setAlgorithm(algorithm);
+ for (SDField structField : getStructFields()) {
+ structField.getMatching().setAlgorithm(algorithm);
+ }
+ }
+
+ /** Adds an explicit index defined in this field */
+ public void addIndex(Index index) {
+ indices.put(index.getName(),index);
+ }
+
+ /**
+ * Returns an index, or null if no index with this name has had
+ * some <b>explicit settings</b> applied in this field (even if this returns null,
+ * the index may be implicitly defined by an indexing statement)
+ */
+ public Index getIndex(String name) {
+ return indices.get(name);
+ }
+
+ /**
+ * Returns an index if this field has one (implicitly or
+ * explicitly) targeting the given name.
+ */
+ public boolean existsIndex(String name) {
+ if (indices.get(name) != null) return true;
+ if (name.equals(getName())) {
+ if (doesIndexing()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Defined indices on this field
+ * @return defined indices on this
+ */
+ public Map<String, Index> getIndices() {
+ return indices;
+ }
+
+ /**
+ * Sets the default rank type of this fields indices, and sets this rank type
+ * to all indices explicitly defined here which has no index set.
+ * (This complex behavior is dues to the fact than we would prefer to have rank types
+ * per field, not per index)
+ */
+ public void setRankType(RankType rankType) {
+ this.rankType=rankType;
+ for (Index index : getIndices().values()) {
+ if (index.getRankType()==null)
+ index.setRankType(rankType);
+ }
+
+ }
+
+ /** Returns the rank settings set in a "rank" block for this field. This is never null. */
+ public Ranking getRanking() { return ranking; }
+
+ /** Returns the default rank type of indices of this field, or null if nothing is set */
+ public RankType getRankType() { return this.rankType; }
+
+ /**
+ * <p>Returns the search-time attribute settings of this field
+ * or null if none is set.</p>
+ *
+ * <p>TODO: Make unmodifiable.</p>
+ */
+ public Map<String, Attribute> getAttributes() { return attributes; }
+
+ public void addAttribute(Attribute attribute) {
+ String name = attribute.getName();
+ if (name == null || "".equals(name)) {
+ name = getName();
+ attribute.setName(name);
+ }
+ attributes.put(attribute.getName(),attribute);
+ }
+
+ /**
+ * Returns the stemming setting of this field.
+ * Default is determined by the owning search definition.
+ *
+ * @return the stemming setting of this, or null, to use the default
+ */
+ public Stemming getStemming() { return stemming; }
+
+ /**
+ * Whether this field should be stemmed in this search definition
+ */
+ public Stemming getStemming(Search search) {
+ if (stemming!=null)
+ return stemming;
+ else
+ return search.getStemming();
+ }
+
+ /**
+ * Sets how this field should be stemmed, or set to null to use the default.
+ */
+ public void setStemming(Stemming stemming) {
+ this.stemming=stemming;
+ }
+
+ /**
+ * List of static summary fields
+ * @return list of static summary fields
+ */
+ public Collection<SummaryField> getSummaryFields() { return summaryFields.values(); }
+
+ /**
+ * Add summary field
+ */
+ public void addSummaryField(SummaryField summaryField) {
+ summaryFields.put(summaryField.getName(),summaryField);
+ }
+
+ /**
+ * Returns a summary field defined (implicitly or explicitly) by this field.
+ * Returns null if there is no such summary field defined.
+ */
+ public SummaryField getSummaryField(String name) {
+ return summaryFields.get(name);
+ }
+
+ /**
+ * Returns a summary field defined (implicitly or explicitly) by this field.
+ *
+ * @param create true to create the summary field and add it to this field before returning if it is missing
+ * @return the summary field, or null if not present and create is false
+ */
+ public SummaryField getSummaryField(String name,boolean create) {
+ SummaryField summaryField=summaryFields.get(name);
+ if (summaryField==null && create) {
+ summaryField=new SummaryField(name, getDataType());
+ addSummaryField(summaryField);
+ }
+ return summaryFields.get(name);
+ }
+
+ /** Returns list of static struct fields */
+ public Collection<SDField> getStructFields() { return structFields.values(); }
+
+ /**
+ * Returns a struct field defined in this field,
+ * potentially traversing into nested structs.
+ * Returns null if there is no such struct field defined.
+ */
+ public SDField getStructField(String name) {
+ if (name.contains(".")) {
+ String superFieldName = name.substring(0,name.indexOf("."));
+ String subFieldName = name.substring(name.indexOf(".")+1);
+ SDField superField = structFields.get(superFieldName);
+ if (superField != null) {
+ return superField.getStructField(subFieldName);
+ }
+ return null;
+ }
+ return structFields.get(name);
+ }
+
+ /**
+ * Returns how the content of this field should be accent normalized etc
+ */
+ public NormalizeLevel getNormalizing() { return normalizing; }
+
+ /**
+ * Change how the content of this field should be accent normalized etc
+ */
+ public void setNormalizing(NormalizeLevel level) { normalizing = level; }
+
+ public void addQueryCommand(String name) {
+ queryCommands.add(name);
+ }
+
+ public boolean hasQueryCommand(String name) {
+ return queryCommands.contains(name);
+ }
+
+ /**
+ * A list of query commands
+ * @return a list of strings with query commands.
+ */
+ public List<String> getQueryCommands() {
+ return queryCommands;
+ }
+
+
+ public boolean isHeaderOrBodyDefined() {
+ return headerOrBodyDefined;
+ }
+
+ public void setHeaderOrBodyDefined(boolean headerOrBodySetExplicitly) {
+ this.headerOrBodyDefined = headerOrBodySetExplicitly;
+ }
+
+ /**
+ * The document that this field was declared in, or null
+ *
+ */
+ public SDDocumentType getOwnerDocType() {
+ return ownerDocType;
+ }
+
+ /**
+ * Two fields are equal if they have the same name
+ * No, they are not.
+ */
+ public boolean equals(Object other) {
+ if ( ! (other instanceof SDField)) return false;
+ return super.equals(other);
+ }
+
+ public int hashCode() {
+ return getName().hashCode();
+ }
+
+ public String toString() {
+ return "field '" + getName() + "'";
+ }
+
+ /** The aliases declared for this field */
+ public Map<String, String> getAliasToName() {
+ return aliasToName;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/Sorting.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/Sorting.java
new file mode 100644
index 00000000000..a0511dec4b0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/Sorting.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import java.io.Serializable;
+
+/**
+ * A search-time document attribute sort specification(per-document in-memory value).
+ * This belongs to the attribute or field(implicitt attribute).
+ *
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public final class Sorting implements Cloneable, Serializable {
+
+ // Remember to change hashCode and equals when you add new fields
+ public enum Function {UCA, RAW, LOWERCASE}
+ public enum Strength {PRIMARY, SECONDARY, TERTIARY, QUATERNARY, IDENTICAL}
+ private boolean ascending = true;
+ private Function function = Function.UCA;
+ private String locale = "";
+ private Strength strength = Strength.PRIMARY;
+
+ public boolean isAscending() { return ascending; }
+ public boolean isDescending() { return ! ascending; }
+ public String getLocale() { return locale; }
+ public Function getFunction() { return function; }
+ public Strength getStrength() { return strength; }
+
+ public void setAscending() { ascending = true; }
+ public void setDescending() { ascending = false; }
+ public void setFunction(Function function) { this.function = function; }
+ public void setLocale(String locale) { this.locale = locale; }
+ public void setStrength(Strength strength) { this.strength = strength; }
+
+ public int hashCode() {
+ return locale.hashCode() +
+ strength.hashCode() +
+ function.hashCode() +
+ (isDescending() ? 13 : 0);
+ }
+
+ public boolean equals(Object object) {
+ if (! (object instanceof Sorting)) return false;
+
+ Sorting other=(Sorting)object;
+ return this.locale.equals(other.locale) &&
+ (ascending == other.ascending) &&
+ (function == other.function) &&
+ (strength == other.strength);
+ }
+
+ public @Override Sorting clone() {
+ try {
+ return (Sorting)super.clone();
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Programming error");
+ }
+ }
+
+ public String toString() {
+ return "sorting '" + (isAscending() ? '+' : '-') + function.toString() + "(" + strength.toString() + ", " + locale + ")";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/Stemming.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/Stemming.java
new file mode 100644
index 00000000000..f471201f55e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/Stemming.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import com.yahoo.language.process.StemMode;
+
+import java.util.logging.Logger;
+
+/**
+ * <p>The stemming setting of a field. This describes how the search engine
+ * should transform content of this field into base forms (stems) to increase
+ * recall (find "car" when you search for "cars" etc.).</p>
+ *
+ * @author bratseth
+ */
+public enum Stemming {
+
+ /** No stemming */
+ NONE("none"),
+
+ /** Stem as much as possible */
+ ALL("all"),
+
+ /** select shortest possible stem */
+ SHORTEST("shortest"),
+
+ /** index (and query?) multiple stems */
+ MULTIPLE("multiple");
+
+ private static Logger log=Logger.getLogger(Stemming.class.getName());
+
+ private final String name;
+
+ /**
+ * Returns the stemming object for the given string.
+ * The legal stemming names are the stemming constants in any capitalization.
+ *
+ * @throws IllegalArgumentException if there is no stemming type with the given name
+ */
+ public static Stemming get(String stemmingName) {
+ try {
+ Stemming stemming = Stemming.valueOf(stemmingName.toUpperCase());
+ if (stemming.equals(ALL)) {
+ log.warning("note: stemming ALL is the same as stemming mode SHORTEST");
+ stemming = SHORTEST;
+ }
+ return stemming;
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("'" + stemmingName + "' is not a valid stemming setting");
+ }
+ }
+
+ private Stemming(String name) {
+ this.name = name;
+ }
+
+ public String getName() { return name; }
+
+ public String toString() {
+ return "stemming " + name;
+ }
+
+ public StemMode toStemMode() {
+ if (this == Stemming.SHORTEST) {
+ return StemMode.SHORTEST;
+ }
+ if (this == Stemming.MULTIPLE) {
+ return StemMode.ALL;
+ }
+ return StemMode.NONE;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDDocumentType.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDDocumentType.java
new file mode 100644
index 00000000000..193ec75acd0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDDocumentType.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import com.yahoo.document.DataTypeName;
+
+/**
+ * @author balder
+ */
+public class TemporarySDDocumentType extends SDDocumentType {
+ public TemporarySDDocumentType(DataTypeName name) {
+ super(name);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDField.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDField.java
new file mode 100644
index 00000000000..00012d65434
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/TemporarySDField.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import com.yahoo.document.DataType;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class TemporarySDField extends SDField {
+
+ public TemporarySDField(String name, DataType dataType, boolean isHeader, SDDocumentType owner) {
+ super(owner, name, dataType, isHeader, owner, false);
+ }
+
+ public TemporarySDField(String name, DataType dataType) {
+ super(null, name, dataType, true, false);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/TypedKey.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/TypedKey.java
new file mode 100644
index 00000000000..6a10ebe642d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/TypedKey.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document;
+
+import com.yahoo.document.DataType;
+
+/**
+ * Common interface for various typed key (or field definitions).
+ * Used by code which wants to use common algorithms for dealing with typed keys, like the logical mapping
+ *
+ * @author bratseth
+ */
+public interface TypedKey {
+
+ String getName();
+
+ void setDataType(DataType type);
+
+ DataType getDataType();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/SDAnnotationType.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/SDAnnotationType.java
new file mode 100644
index 00000000000..0f8e200c4c6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/SDAnnotationType.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document.annotation;
+
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.document.annotation.AnnotationType;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class SDAnnotationType extends AnnotationType {
+ private SDDocumentType sdDocType;
+ private String inherits;
+
+ public SDAnnotationType(String name) {
+ super(name);
+ }
+
+ public SDAnnotationType(String name, SDDocumentType dataType, String inherits) {
+ super(name);
+ this.sdDocType = dataType;
+ this.inherits = inherits;
+ }
+
+ public SDDocumentType getSdDocType() {
+ return sdDocType;
+ }
+
+ public String getInherits() {
+ return inherits;
+ }
+
+ public void inherit(String inherits) {
+ this.inherits = inherits;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/TemporaryAnnotationReferenceDataType.java b/config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/TemporaryAnnotationReferenceDataType.java
new file mode 100644
index 00000000000..5b82b068908
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/document/annotation/TemporaryAnnotationReferenceDataType.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.document.annotation;
+
+import com.yahoo.document.annotation.AnnotationReferenceDataType;
+import com.yahoo.document.annotation.AnnotationType;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class TemporaryAnnotationReferenceDataType extends AnnotationReferenceDataType {
+ private final String target;
+
+ public TemporaryAnnotationReferenceDataType(String target) {
+ this.target = target;
+ }
+
+ public String getTarget() {
+ return target;
+ }
+
+ @Override
+ public void setAnnotationType(AnnotationType type) {
+ super.setName("annotationreference<" + type.getName() + ">");
+ super.setAnnotationType(type);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AliasOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AliasOperation.java
new file mode 100644
index 00000000000..60aeb3d50cc
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AliasOperation.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class AliasOperation implements FieldOperation {
+ private String aliasedName;
+ private String alias;
+
+ public AliasOperation(String aliasedName, String alias) {
+ this.aliasedName = aliasedName;
+ this.alias = alias;
+ }
+
+ public String getAliasedName() {
+ return aliasedName;
+ }
+
+ public void setAliasedName(String aliasedName) {
+ this.aliasedName = aliasedName;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public void apply(SDField field) {
+ if (aliasedName == null) {
+ aliasedName = field.getName();
+ }
+ field.getAliasToName().put(alias, aliasedName);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AttributeOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AttributeOperation.java
new file mode 100644
index 00000000000..19ee857c627
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/AttributeOperation.java
@@ -0,0 +1,153 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.tensor.TensorType;
+
+import java.util.Optional;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class AttributeOperation implements FieldOperation, FieldOperationContainer {
+ private final String name;
+ private Boolean huge;
+ private Boolean fastSearch;
+ private Boolean fastAccess;
+ private Boolean prefetch;
+ private Boolean enableBitVectors;
+ private Boolean enableOnlyBitVector;
+ //TODO: Husk sorting!!
+ private boolean doAlias = false;
+ private String alias;
+ private String aliasedName;
+ private Optional<TensorType> tensorType = Optional.empty();
+
+ public AttributeOperation(String name) {
+ this.name = name;
+ }
+
+ public void addOperation(FieldOperation op) {
+ //TODO: Implement this method:
+
+ }
+
+ public void applyOperations(SDField field) {
+ //TODO: Implement this method:
+
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Boolean getHuge() {
+ return huge;
+ }
+
+ public void setHuge(Boolean huge) {
+ this.huge = huge;
+ }
+
+ public Boolean getFastSearch() {
+ return fastSearch;
+ }
+
+ public void setFastSearch(Boolean fastSearch) {
+ this.fastSearch = fastSearch;
+ }
+
+ public Boolean getFastAccess() {
+ return fastAccess;
+ }
+
+ public void setFastAccess(Boolean fastAccess) {
+ this.fastAccess = fastAccess;
+ }
+
+ public Boolean getPrefetch() {
+ return prefetch;
+ }
+
+ public void setPrefetch(Boolean prefetch) {
+ this.prefetch = prefetch;
+ }
+
+ public Boolean getEnableBitVectors() {
+ return enableBitVectors;
+ }
+
+ public void setEnableBitVectors(Boolean enableBitVectors) {
+ this.enableBitVectors = enableBitVectors;
+ }
+
+ public Boolean getEnableOnlyBitVector() {
+ return enableOnlyBitVector;
+ }
+
+ public void setEnableOnlyBitVector(Boolean enableOnlyBitVector) {
+ this.enableOnlyBitVector = enableOnlyBitVector;
+ }
+
+ public boolean isDoAlias() {
+ return doAlias;
+ }
+
+ public void setDoAlias(boolean doAlias) {
+ this.doAlias = doAlias;
+ }
+
+ public String getAlias() {
+ return alias;
+ }
+
+ public void setAlias(String alias) {
+ this.alias = alias;
+ }
+
+ public String getAliasedName() {
+ return aliasedName;
+ }
+
+ public void setAliasedName(String aliasedName) {
+ this.aliasedName = aliasedName;
+ }
+
+ public void setTensorType(TensorType tensorType) {
+ this.tensorType = Optional.of(tensorType);
+ }
+
+ public void apply(SDField field) {
+ Attribute attribute = field.getAttributes().get(name);
+ if (attribute == null) {
+ attribute = new Attribute(name, field.getDataType());
+ field.addAttribute(attribute);
+ }
+
+ if (huge != null) {
+ attribute.setHuge(huge);
+ }
+ if (fastSearch != null) {
+ attribute.setFastSearch(fastSearch);
+ }
+ if (fastAccess != null) {
+ attribute.setFastAccess(fastAccess);
+ }
+ if (prefetch != null) {
+ attribute.setPrefetch(prefetch);
+ }
+ if (enableBitVectors != null) {
+ attribute.setEnableBitVectors(enableBitVectors);
+ }
+ if (enableOnlyBitVector != null) {
+ attribute.setEnableOnlyBitVector(enableOnlyBitVector);
+ }
+ if (doAlias) {
+ field.getAliasToName().put(alias, aliasedName);
+ }
+ if (tensorType.isPresent()) {
+ attribute.setTensorType(tensorType.get());
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BodyOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BodyOperation.java
new file mode 100644
index 00000000000..b830407ea2e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BodyOperation.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class BodyOperation implements FieldOperation {
+ public void apply(SDField field) {
+ field.setHeader(false);
+ field.setHeaderOrBodyDefined(true);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BoldingOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BoldingOperation.java
new file mode 100644
index 00000000000..f6c7277869e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/BoldingOperation.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class BoldingOperation implements FieldOperation {
+
+ private final boolean bold;
+
+ public BoldingOperation(boolean bold) {
+ this.bold = bold;
+ }
+
+ public void apply(SDField field) {
+ SummaryField summaryField = field.getSummaryField(field.getName(), true);
+ summaryField.addSource(field.getName());
+ summaryField.addDestination("default");
+ summaryField.setTransform(bold ? summaryField.getTransform().bold() : summaryField.getTransform().unbold());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperation.java
new file mode 100644
index 00000000000..20caf0b13c7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperation.java
@@ -0,0 +1,11 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public interface FieldOperation {
+ public void apply(SDField field);
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperationContainer.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperationContainer.java
new file mode 100644
index 00000000000..30d16e369b1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/FieldOperationContainer.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public interface FieldOperationContainer {
+ public void addOperation(FieldOperation op);
+ public void applyOperations(SDField field);
+ public String getName();
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/HeaderOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/HeaderOperation.java
new file mode 100644
index 00000000000..48d2711b39e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/HeaderOperation.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class HeaderOperation implements FieldOperation {
+ public void apply(SDField field) {
+ field.setHeader(true);
+ field.setHeaderOrBodyDefined(true);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IdOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IdOperation.java
new file mode 100644
index 00000000000..25318e2db85
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IdOperation.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class IdOperation implements FieldOperation {
+ private SDDocumentType document;
+ private int fieldId;
+
+ public SDDocumentType getDocument() {
+ return document;
+ }
+
+ public void setDocument(SDDocumentType document) {
+ this.document = document;
+ }
+
+ public int getFieldId() {
+ return fieldId;
+ }
+
+ public void setFieldId(int fieldId) {
+ this.fieldId = fieldId;
+ }
+
+ public void apply(SDField field) {
+ document.setFieldId(field, fieldId);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java
new file mode 100644
index 00000000000..8ab065ce419
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexOperation.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Index.Type;
+import com.yahoo.searchdefinition.document.BooleanIndexDefinition;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class IndexOperation implements FieldOperation {
+ private String indexName;
+ private Optional<Boolean> prefix = Optional.empty();
+ private List<String> aliases = new LinkedList<>();
+ private Optional<String> stemming = Optional.empty();
+ private Optional<Type> type = Optional.empty();
+
+ private OptionalInt arity = OptionalInt.empty(); // For predicate data type in boolean search
+ private OptionalLong lowerBound = OptionalLong.empty();
+ private OptionalLong upperBound = OptionalLong.empty();
+ private OptionalDouble densePostingListThreshold = OptionalDouble.empty();
+
+ public String getIndexName() {
+ return indexName;
+ }
+
+ public void setIndexName(String indexName) {
+ this.indexName = indexName;
+ }
+
+ public boolean getPrefix() {
+ return prefix.get();
+ }
+
+ public void setPrefix(Boolean prefix) {
+ this.prefix = Optional.of(prefix);
+ }
+
+ public void addAlias(String alias) {
+ aliases.add(alias);
+ }
+
+ public String getStemming() {
+ return stemming.get();
+ }
+
+ public void setStemming(String stemming) {
+ this.stemming = Optional.of(stemming);
+ }
+
+ public void apply(SDField field) {
+ Index index = field.getIndex(indexName);
+
+ if (index == null) {
+ index = new Index(indexName);
+ field.addIndex(index);
+ }
+
+ applyToIndex(index);
+ }
+
+ public void applyToIndex(Index index) {
+ if (prefix.isPresent()) {
+ index.setPrefix(prefix.get());
+ }
+ for (String alias : aliases) {
+ index.addAlias(alias);
+ }
+ if (stemming.isPresent()) {
+ index.setStemming(Stemming.get(stemming.get()));
+ }
+ if (type.isPresent()) {
+ index.setType(type.get());
+ }
+ if (arity.isPresent() || lowerBound.isPresent() ||
+ upperBound.isPresent() || densePostingListThreshold.isPresent()) {
+ index.setBooleanIndexDefiniton(
+ new BooleanIndexDefinition(arity, lowerBound, upperBound, densePostingListThreshold));
+ }
+ }
+
+ public Type getType() {
+ return type.get();
+ }
+
+ public void setType(Type type) {
+ this.type = Optional.of(type);
+ }
+
+ public void setArity(int arity) {
+ this.arity = OptionalInt.of(arity);
+ }
+
+ public void setLowerBound(long value) {
+ this.lowerBound = OptionalLong.of(value);
+ }
+
+ public void setUpperBound(long value) {
+ this.upperBound = OptionalLong.of(value);
+ }
+
+ public void setDensePostingListThreshold(double densePostingListThreshold) {
+ this.densePostingListThreshold = OptionalDouble.of(densePostingListThreshold);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingOperation.java
new file mode 100644
index 00000000000..af3852bffb8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingOperation.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.language.Linguistics;
+import com.yahoo.language.simple.SimpleLinguistics;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.searchdefinition.parser.SimpleCharStream;
+import com.yahoo.vespa.indexinglanguage.ScriptParserContext;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.StatementExpression;
+import com.yahoo.vespa.indexinglanguage.linguistics.AnnotatorConfig;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class IndexingOperation implements FieldOperation {
+
+ private final ScriptExpression script;
+
+ public IndexingOperation(ScriptExpression script) {
+ this.script = script;
+ }
+
+ public void apply(SDField field) {
+ field.setIndexingScript(script);
+ }
+
+ /** Creates an indexing operation which will use the simple linguistics implementation suitable for testing */
+ public static IndexingOperation fromStream(SimpleCharStream input, boolean multiLine) throws ParseException {
+ return fromStream(input, multiLine, new SimpleLinguistics());
+ }
+
+ public static IndexingOperation fromStream(SimpleCharStream input, boolean multiLine, Linguistics linguistics)
+ throws ParseException {
+ ScriptParserContext config = new ScriptParserContext(linguistics);
+ config.setAnnotatorConfig(new AnnotatorConfig());
+ config.setInputStream(input);
+ ScriptExpression exp;
+ try {
+ if (multiLine) {
+ exp = ScriptExpression.newInstance(config);
+ } else {
+ exp = new ScriptExpression(StatementExpression.newInstance(config));
+ }
+ } catch (com.yahoo.vespa.indexinglanguage.parser.ParseException e) {
+ ParseException t = new ParseException("Error reported by IL parser: " + e.getMessage());
+ t.initCause(e);
+ throw t;
+ }
+ return new IndexingOperation(exp);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingRewriteOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingRewriteOperation.java
new file mode 100644
index 00000000000..a416895a6a7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/IndexingRewriteOperation.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class IndexingRewriteOperation implements FieldOperation {
+ public void apply(SDField field) {
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/MatchOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/MatchOperation.java
new file mode 100644
index 00000000000..9128778b7ea
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/MatchOperation.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class MatchOperation implements FieldOperation {
+ private Matching.Type matchingType;
+ private Integer gramSize;
+ private Integer maxLength;
+ private Matching.Algorithm matchingAlgorithm;
+ private String exactMatchTerminator;
+
+ public void setMatchingType(Matching.Type matchingType) {
+ this.matchingType = matchingType;
+ }
+
+ public void setGramSize(Integer gramSize) {
+ this.gramSize = gramSize;
+ }
+ public void setMaxLength(Integer maxLength) {
+ this.maxLength = maxLength;
+ }
+
+ public void setMatchingAlgorithm(Matching.Algorithm matchingAlgorithm) {
+ this.matchingAlgorithm = matchingAlgorithm;
+ }
+
+ public void setExactMatchTerminator(String exactMatchTerminator) {
+ this.exactMatchTerminator = exactMatchTerminator;
+ }
+
+ public void apply(SDField field) {
+ if (matchingType != null) {
+ field.setMatchingType(matchingType);
+ }
+ if (gramSize != null) {
+ field.getMatching().setGramSize(gramSize);
+ }
+ if (maxLength != null) {
+ field.getMatching().maxLength(maxLength);
+ }
+ if (matchingAlgorithm != null) {
+ field.setMatchingAlgorithm(matchingAlgorithm);
+ }
+ if (exactMatchTerminator != null) {
+ field.getMatching().setExactMatchTerminator(exactMatchTerminator);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/NormalizingOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/NormalizingOperation.java
new file mode 100644
index 00000000000..e0d15cabbe0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/NormalizingOperation.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.NormalizeLevel;
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class NormalizingOperation implements FieldOperation {
+ private NormalizeLevel.Level level;
+
+ public NormalizingOperation(String setting) {
+ if ("none".equals(setting)) {
+ this.level = NormalizeLevel.Level.NONE;
+ } else if ("codepoint".equals(setting)) {
+ this.level = NormalizeLevel.Level.CODEPOINT;
+ } else if ("lowercase".equals(setting)) {
+ this.level = NormalizeLevel.Level.LOWERCASE;
+ } else if ("accent".equals(setting)) {
+ this.level = NormalizeLevel.Level.ACCENT;
+ } else if ("all".equals(setting)) {
+ this.level = NormalizeLevel.Level.ACCENT;
+ } else {
+ throw new IllegalArgumentException("invalid normalizing setting: "+setting);
+ }
+ }
+
+ public void apply(SDField field) {
+ field.setNormalizing(new NormalizeLevel(level, true));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/QueryCommandOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/QueryCommandOperation.java
new file mode 100644
index 00000000000..e4af5660e7b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/QueryCommandOperation.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class QueryCommandOperation implements FieldOperation {
+ private List<String> queryCommands = new java.util.ArrayList<>(0);
+
+ public void addQueryCommand(String name) {
+ queryCommands.add(name);
+ }
+
+ public void apply(SDField field) {
+ for (String command : queryCommands) {
+ field.addQueryCommand(command);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankOperation.java
new file mode 100644
index 00000000000..42b1e5514a1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankOperation.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class RankOperation implements FieldOperation {
+
+ private Boolean literal = null;
+ private Boolean filter = null;
+ private Boolean normal = null;
+
+ public Boolean getLiteral() { return literal; }
+ public void setLiteral(Boolean literal) { this.literal = literal; }
+
+ public Boolean getFilter() { return filter; }
+ public void setFilter(Boolean filter) { this.filter = filter; }
+
+ public Boolean getNormal() { return normal; }
+ public void setNormal(Boolean n) { this.normal = n; }
+
+ public void apply(SDField field) {
+ if (literal != null) {
+ field.getRanking().setLiteral(literal);
+ }
+ if (filter != null) {
+ field.getRanking().setFilter(filter);
+ }
+ if (normal != null) {
+ field.getRanking().setNormal(normal);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankTypeOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankTypeOperation.java
new file mode 100644
index 00000000000..640ccc48818
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/RankTypeOperation.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Index;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class RankTypeOperation implements FieldOperation {
+ private String indexName;
+ private RankType type;
+
+ public String getIndexName() {
+ return indexName;
+ }
+ public void setIndexName(String indexName) {
+ this.indexName = indexName;
+ }
+
+ public RankType getType() {
+ return type;
+ }
+ public void setType(RankType type) {
+ this.type = type;
+ }
+
+ public void apply(SDField field) {
+ if (indexName == null) {
+ field.setRankType(type); // Set default if the index is not specified.
+ } else {
+ Index index = field.getIndex(indexName);
+ if (index == null) {
+ index = new Index(indexName);
+ field.addIndex(index);
+ }
+ index.setRankType(type);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SortingOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SortingOperation.java
new file mode 100644
index 00000000000..e9225f0def5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SortingOperation.java
@@ -0,0 +1,91 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Sorting;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class SortingOperation implements FieldOperation {
+ private String attributeName;
+ private Boolean ascending;
+ private Boolean descending;
+ private Sorting.Function function;
+ private Sorting.Strength strength;
+ private String locale;
+
+ public SortingOperation(String attributeName) {
+ this.attributeName = attributeName;
+ }
+
+ public String getAttributeName() {
+ return attributeName;
+ }
+
+ public Boolean getAscending() {
+ return ascending;
+ }
+
+ public void setAscending() {
+ this.ascending = true;
+ }
+
+ public Boolean getDescending() {
+ return descending;
+ }
+
+ public void setDescending() {
+ this.descending = true;
+ }
+
+ public Sorting.Function getFunction() {
+ return function;
+ }
+
+ public void setFunction(Sorting.Function function) {
+ this.function = function;
+ }
+
+ public Sorting.Strength getStrength() {
+ return strength;
+ }
+
+ public void setStrength(Sorting.Strength strength) {
+ this.strength = strength;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+
+ public void setLocale(String locale) {
+ this.locale = locale;
+ }
+
+ public void apply(SDField field) {
+ Attribute attribute = field.getAttributes().get(attributeName);
+ if (attribute == null) {
+ attribute = new Attribute(attributeName, field.getDataType());
+ field.addAttribute(attribute);
+ }
+ Sorting sorting = attribute.getSorting();
+
+ if (ascending != null) {
+ sorting.setAscending();
+ }
+ if (descending != null) {
+ sorting.setDescending();
+ }
+ if (function != null) {
+ sorting.setFunction(function);
+ }
+ if (strength != null) {
+ sorting.setStrength(strength);
+ }
+ if (locale != null) {
+ sorting.setLocale(locale);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StemmingOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StemmingOperation.java
new file mode 100644
index 00000000000..19b760386a1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StemmingOperation.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class StemmingOperation implements FieldOperation {
+ private String setting;
+
+ public String getSetting() {
+ return setting;
+ }
+
+ public void setSetting(String setting) {
+ this.setting = setting;
+ }
+
+ public void apply(SDField field) {
+ field.setStemming(Stemming.get(setting));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StructFieldOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StructFieldOperation.java
new file mode 100644
index 00000000000..f0c3d964934
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/StructFieldOperation.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class StructFieldOperation implements FieldOperation, FieldOperationContainer {
+ private String structFieldName;
+ private List<FieldOperation> pendingOperations = new LinkedList<>();
+
+ public StructFieldOperation(String structFieldName) {
+ this.structFieldName = structFieldName;
+ }
+
+ public void apply(SDField field) {
+ SDField structField = field.getStructField(structFieldName);
+ if (structField == null ) {
+ throw new IllegalArgumentException("Struct field '" + structFieldName + "' has not been defined in struct " +
+ "for field '" + field.getName() + "'.");
+ }
+
+ applyOperations(structField);
+ }
+
+ public void addOperation(FieldOperation op) {
+ pendingOperations.add(op);
+ }
+
+ public void applyOperations(SDField field) {
+ if (pendingOperations.isEmpty()) {
+ return;
+ }
+ ListIterator<FieldOperation> ops = pendingOperations.listIterator();
+ while (ops.hasNext()) {
+ FieldOperation op = ops.next();
+ ops.remove();
+ op.apply(field);
+ }
+ }
+
+ public String getName() {
+ return structFieldName;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldLongOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldLongOperation.java
new file mode 100644
index 00000000000..f320ef649b4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldLongOperation.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class SummaryInFieldLongOperation extends SummaryInFieldOperation {
+ private DataType type;
+ private Boolean bold;
+ private Set<String> destinations = new java.util.LinkedHashSet<>();
+ private List<SummaryField.Property> properties = new ArrayList<>();
+
+ public SummaryInFieldLongOperation(String name) {
+ super(name);
+ }
+
+ public SummaryInFieldLongOperation() {
+ super(null);
+ }
+
+ public void setType(DataType type) {
+ this.type = type;
+ }
+
+ public void setBold(Boolean bold) {
+ this.bold = bold;
+ }
+
+ public void addDestination(String destination) {
+ destinations.add(destination);
+ }
+
+ public Iterator<String> destinationIterator() {
+ return destinations.iterator();
+ }
+
+
+ public void addProperty(SummaryField.Property property) {
+ properties.add(property);
+ }
+
+ public void apply(SDField field) {
+ if (type == null) {
+ type = field.getDataType();
+ }
+ SummaryField summary = new SummaryField(name, type);
+ applyToSummary(summary);
+ field.addSummaryField(summary);
+ }
+
+ public void applyToSummary(SummaryField summary) {
+ if (transform != null) {
+ summary.setTransform(transform);
+ }
+
+ if (bold != null) {
+ summary.setTransform(bold ? summary.getTransform().bold() : summary.getTransform().unbold());
+ }
+
+ for (SummaryField.Source source : sources) {
+ summary.addSource(source);
+ }
+
+ for (String destination : destinations) {
+ summary.addDestination(destination);
+ }
+
+ for (SummaryField.Property prop : properties) {
+ summary.addProperty(prop.getName(), prop.getValue());
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldOperation.java
new file mode 100644
index 00000000000..e1abbcc93b5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldOperation.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public abstract class SummaryInFieldOperation implements FieldOperation {
+ protected String name;
+ protected SummaryTransform transform;
+ protected Set<SummaryField.Source> sources = new java.util.LinkedHashSet<>();
+
+ public SummaryInFieldOperation(String name) {
+ this.name = name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setTransform(SummaryTransform transform) {
+ this.transform = transform;
+ }
+
+ public SummaryTransform getTransform() {
+ return transform;
+ }
+
+ public void addSource(String name) {
+ sources.add(new SummaryField.Source(name));
+ }
+
+ public void addSource(SummaryField.Source source) {
+ sources.add(source);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldShortOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldShortOperation.java
new file mode 100644
index 00000000000..1ffb8d9a83e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryInFieldShortOperation.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class SummaryInFieldShortOperation extends SummaryInFieldOperation {
+ public SummaryInFieldShortOperation(String name) {
+ super(name);
+ }
+
+ public void apply(SDField field) {
+ SummaryField ret = field.getSummaryField(name);
+ if (ret == null) {
+ ret = new SummaryField(name, field.getDataType());
+ ret.addSource(field.getName());
+ ret.addDestination("default");
+ }
+ ret.setImplicit(false);
+
+ ret.setTransform(transform);
+ for (SummaryField.Source source : sources) {
+ ret.addSource(source);
+ }
+ field.addSummaryField(ret);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryToOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryToOperation.java
new file mode 100644
index 00000000000..f392b2fdcc8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/SummaryToOperation.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class SummaryToOperation implements FieldOperation {
+ private Set<String> destinations = new java.util.LinkedHashSet<>();
+ private String name;
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void addDestination(String destination) {
+ destinations.add(destination);
+ }
+
+ public void apply(SDField field) {
+ SummaryField summary;
+ summary = field.getSummaryField(name);
+ if (summary == null) {
+ summary = new SummaryField(field);
+ summary.addSource(field.getName());
+ summary.addDestination("default");
+ field.addSummaryField(summary);
+ }
+ summary.setImplicit(false);
+
+ for (String destination : destinations) {
+ summary.addDestination(destination);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightOperation.java
new file mode 100644
index 00000000000..3cbd557b850
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightOperation.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.SDField;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class WeightOperation implements FieldOperation {
+ private int weight;
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public void setWeight(int weight) {
+ this.weight = weight;
+ }
+
+ public void apply(SDField field) {
+ field.setWeight(weight);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightedSetOperation.java b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightedSetOperation.java
new file mode 100644
index 00000000000..c1721a4dbb5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/fieldoperation/WeightedSetOperation.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.fieldoperation;
+
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.document.WeightedSetDataType;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class WeightedSetOperation implements FieldOperation {
+
+ private Boolean createIfNonExistent;
+ private Boolean removeIfZero;
+
+ public Boolean getCreateIfNonExistent() {
+ return createIfNonExistent;
+ }
+
+ public void setCreateIfNonExistent(Boolean createIfNonExistent) {
+ this.createIfNonExistent = createIfNonExistent;
+ }
+
+ public Boolean getRemoveIfZero() {
+ return removeIfZero;
+ }
+
+ public void setRemoveIfZero(Boolean removeIfZero) {
+ this.removeIfZero = removeIfZero;
+ }
+
+ public void apply(SDField field) {
+ WeightedSetDataType ctype = (WeightedSetDataType) field.getDataType();
+
+ if (createIfNonExistent != null) {
+ field.setDataType(DataType.getWeightedSet(ctype.getNestedType(), createIfNonExistent,
+ ctype.removeIfZero()));
+ }
+
+ ctype = (WeightedSetDataType) field.getDataType();
+ if (removeIfZero != null) {
+ field.setDataType(DataType.getWeightedSet(ctype.getNestedType(),
+ ctype.createIfNonExistent(), removeIfZero));
+ }
+
+ ctype = (WeightedSetDataType) field.getDataType();
+ for (Object o : field.getAttributes().values()) {
+ Attribute attribute = (Attribute) o;
+ attribute.setRemoveIfZero(ctype.removeIfZero());
+ attribute.setCreateIfNonExistent(ctype.createIfNonExistent());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "WeightedSetOperation{" +
+ "createIfNonExistent=" + createIfNonExistent +
+ ", removeIfZero=" + removeIfZero +
+ '}';
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/parser/SimpleCharStream.java b/config-model/src/main/java/com/yahoo/searchdefinition/parser/SimpleCharStream.java
new file mode 100644
index 00000000000..b925b4f2caa
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/parser/SimpleCharStream.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.parser;
+
+import com.yahoo.javacc.FastCharStream;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+@SuppressWarnings("deprecation")
+public class SimpleCharStream extends FastCharStream implements com.yahoo.searchdefinition.parser.CharStream,
+ com.yahoo.vespa.indexinglanguage.parser.CharStream
+{
+ public SimpleCharStream(String input) {
+ super(input);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/AddExtraFieldsToDocument.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/AddExtraFieldsToDocument.java
new file mode 100644
index 00000000000..5d39c70e3b9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/AddExtraFieldsToDocument.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * This processor creates a {@link com.yahoo.searchdefinition.document.SDDocumentType} for each {@link Search} object which holds all the data that search
+ * associates with a document described in a search definition file. This includes all extra fields, summary fields and
+ * implicit fields. All non-indexed and non-summary fields are discarded.
+ */
+public class AddExtraFieldsToDocument extends Processor {
+
+ public AddExtraFieldsToDocument(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ SDDocumentType document = search.getDocument();
+ if (document != null) {
+ for (Field field : search.extraFieldList()) {
+ addSdField(search, document, (SDField)field);
+ }
+ for (SummaryField field : search.getSummary("default").getSummaryFields()) {
+ addSummaryField(search, document, field);
+ }
+ }
+ }
+
+ private void addSdField(Search search, SDDocumentType document, SDField field) {
+ if (field.getIndexToCount() == 0 && field.getAttributes().isEmpty()) {
+ return;
+ }
+ for (Attribute atr : field.getAttributes().values()) {
+ if (atr.getName().equals(field.getName() + "_position")) {
+ DataType type = PositionDataType.INSTANCE;
+ if (atr.getCollectionType().equals(Attribute.CollectionType.ARRAY)) {
+ type = DataType.getArray(type);
+ }
+ addField(search, document, new SDField(document, atr.getName(), type));
+ } else if (!atr.getName().equals(field.getName())) {
+ addField(search, document, new SDField(document, atr.getName(), atr.getDataType()));
+ }
+ }
+ addField(search, document, field);
+ }
+
+ private void addSummaryField(Search search, SDDocumentType document, SummaryField field) {
+ Field docField = document.getField(field.getName());
+ if (docField == null) {
+ SDField newField = search.getField(field.getName());
+ if (newField == null) {
+ newField = new SDField(document, field.getName(), field.getDataType(), field.isHeader(), true);
+ newField.setIsExtraField(true);
+ }
+ document.addField(newField);
+ } else if (!docField.getDataType().equals(field.getDataType())) {
+ throw newProcessException(search, field, "Summary field has conflicting type.");
+ }
+ }
+
+ private void addField(Search search, SDDocumentType document, Field field) {
+ if (document.getField(field.getName()) != null) {
+ throw newProcessException(search, field, "Field shadows another.");
+ }
+ document.addField(field);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributeProperties.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributeProperties.java
new file mode 100644
index 00000000000..9df77c09373
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributeProperties.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Checks that attribute properties only are set for attributes that have data (are created by an indexing statement).
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class AttributeProperties extends Processor {
+
+ public AttributeProperties(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ String fieldName = field.getName();
+
+ // For each attribute, check if the attribute has been created
+ // by an indexing statement.
+ for (Attribute attribute : field.getAttributes().values()) {
+ if (attributeCreated(field, attribute.getName())) {
+ continue;
+ }
+ // Check other fields or statements that may have created this attribute.
+ boolean created = false;
+ for (SDField f : search.allFieldsList()) {
+ // Checking against the field we are looking at
+ if (!f.getName().equals(fieldName)) {
+ if (attributeCreated(f, attribute.getName())) {
+ created = true;
+ break;
+ }
+ }
+ }
+ if (!created) {
+ throw new IllegalArgumentException("Attribute '" + attribute.getName() + "' in field '" +
+ field.getName() + "' is not created by the indexing statement");
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks if the attribute has been created bye an indexing statement in this field.
+ *
+ * @param field a searchdefinition field
+ * @param attributeName name of the attribute
+ * @return true if the attribute has been created by this field, else false
+ */
+ static boolean attributeCreated(SDField field, String attributeName) {
+ if (!field.doesAttributing()) {
+ return false;
+ }
+ for (Attribute attribute : field.getAttributes().values()) {
+ if (attribute.getName().equals(attributeName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributesImplicitWord.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributesImplicitWord.java
new file mode 100644
index 00000000000..774160793cb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/AttributesImplicitWord.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.document.NumericDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Fields that derive to attribute(s) and no indices should use the WORD indexing form,
+ * in a feeble attempt to match the most peoples expectations as closely as possible.
+ *
+ * @author vegardh
+ *
+ */
+public class AttributesImplicitWord extends Processor {
+
+ public AttributesImplicitWord(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (fieldImplicitlyWordMatch(field)) {
+ field.getMatching().setType(Matching.Type.WORD);
+ }
+ }
+ }
+
+ private boolean fieldImplicitlyWordMatch(SDField field) {
+ // numeric types should not trigger exact-match query parsing
+ DataType dt = field.getDataType().getPrimitiveType();
+ if (dt != null && dt instanceof NumericDataType) {
+ return false;
+ }
+ return (field.getIndexToCount() == 0
+ && field.getAttributes().size() > 0
+ && field.getIndices().isEmpty()
+ && !field.getMatching().isTypeUserSet());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/Bolding.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Bolding.java
new file mode 100644
index 00000000000..2e2e665eb34
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Bolding.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Checks that bolding or dynamic summary is turned on only for text fields. Throws exception if it is turned on for any
+ * other fields (otherwise will cause indexing failure)
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class Bolding extends Processor {
+
+ public Bolding(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ for (SummaryField summary : field.getSummaryFields()) {
+ if (summary.getTransform().isBolded() &&
+ !((summary.getDataType() == DataType.STRING) || (summary.getDataType() == DataType.URI)))
+ {
+ throw new IllegalArgumentException("'bolding: on' for non-text field " +
+ "'" + field.getName() + "'" +
+ " (" + summary.getDataType() + ")" +
+ " is not allowed");
+ } else if (summary.getTransform().isDynamic() &&
+ !((summary.getDataType() == DataType.STRING) || (summary.getDataType() == DataType.URI)))
+ {
+ throw new IllegalArgumentException("'summary: dynamic' for non-text field " +
+ "'" + field.getName() + "'" +
+ " (" + summary.getDataType() + ")" +
+ " is not allowed");
+ }
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/BuiltInFieldSets.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/BuiltInFieldSets.java
new file mode 100644
index 00000000000..4ad9b5304a5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/BuiltInFieldSets.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Adds field sets for 1) fields defined inside document type 2) fields inside search but outside document
+ * @author vegardh
+ *
+ */
+public class BuiltInFieldSets extends Processor {
+
+ private static final String DOC_FIELDSET_NAME = "[document]";
+ public static final String SEARCH_FIELDSET_NAME = "[search]"; // Public due to oddities in position handling.
+ public static final String INTERNAL_FIELDSET_NAME = "[internal]"; // This one populated from misc places
+
+ public BuiltInFieldSets(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ addDocumentFieldSet();
+ addSearchFieldSet();
+ // "Hook" the field sets on search onto the document types, since we will include them
+ // on the document configs
+ search.getDocument().setFieldSets(search.fieldSets());
+ }
+
+ private void addSearchFieldSet() {
+ for (SDField searchField : search.extraFieldList()) {
+ search.fieldSets().addBuiltInFieldSetItem(SEARCH_FIELDSET_NAME, searchField.getName());
+ }
+ }
+
+ private void addDocumentFieldSet() {
+ for (Field docField : search.getDocument().fieldSet()) {
+ search.fieldSets().addBuiltInFieldSetItem(DOC_FIELDSET_NAME, docField.getName());
+ }
+ }
+
+
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/CreatePositionZCurve.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/CreatePositionZCurve.java
new file mode 100644
index 00000000000..0aabd4dc192
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/CreatePositionZCurve.java
@@ -0,0 +1,201 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.ArrayDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.expressions.AttributeExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.expressions.ForEachExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.StatementExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.SummaryExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.ZCurveExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+
+/**
+ * Adds a "fieldName.zcurve" long attribute and a "fieldName.distance" summary field to all position type fields.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class CreatePositionZCurve extends Processor {
+
+ public CreatePositionZCurve(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ DataType fieldType = field.getDataType();
+ if ( ! isSupportedPositionType(fieldType)) continue;
+
+ if (field.doesIndexing()) {
+ fail(search, field, "Indexing of data type '" + fieldType.getName() + "' is not supported, " +
+ "replace 'index' statement with 'attribute'.");
+ }
+
+ if ( ! field.doesAttributing()) continue;
+
+ boolean doesSummary = field.doesSummarying();
+
+ String fieldName = field.getName();
+ field.getAttributes().remove(fieldName);
+
+ String zName = PositionDataType.getZCurveFieldName(fieldName);
+ SDField zCurveField = createZCurveField(field, zName);
+ search.addExtraField(zCurveField);
+ search.fieldSets().addBuiltInFieldSetItem(BuiltInFieldSets.INTERNAL_FIELDSET_NAME, zCurveField.getName());
+
+ // configure summary
+ Collection<String> summaryTo = removeSummaryTo(field);
+ ensureCompatibleSummary(field, zName,
+ PositionDataType.getPositionSummaryFieldName(fieldName),
+ DataType.getArray(DataType.STRING), // will become "xmlstring"
+ SummaryTransform.POSITIONS, summaryTo);
+ ensureCompatibleSummary(field, zName,
+ PositionDataType.getDistanceSummaryFieldName(fieldName),
+ DataType.INT,
+ SummaryTransform.DISTANCE, summaryTo);
+ // clear indexing script
+ field.setIndexingScript(null);
+ SDField posX = field.getStructField(PositionDataType.FIELD_X);
+ if (posX != null) {
+ posX.setIndexingScript(null);
+ }
+ SDField posY = field.getStructField(PositionDataType.FIELD_Y);
+ if (posY != null) {
+ posY.setIndexingScript(null);
+ }
+ if (doesSummary) ensureCompatibleSummary(field, zName,
+ field.getName(),
+ field.getDataType(),
+ SummaryTransform.GEOPOS, summaryTo);
+ }
+ }
+
+ private SDField createZCurveField(SDField inputField, String fieldName) {
+ if (search.getField(fieldName) != null || search.getAttribute(fieldName) != null) {
+ throw newProcessException(search, null, "Incompatible position attribute '" + fieldName +
+ "' already created.");
+ }
+ boolean isArray = inputField.getDataType() instanceof ArrayDataType;
+ SDField field = new SDField(fieldName, isArray ? DataType.getArray(DataType.LONG) : DataType.LONG);
+ Attribute attribute = new Attribute(fieldName, Attribute.Type.LONG, isArray ? Attribute.CollectionType.ARRAY :
+ Attribute.CollectionType.SINGLE);
+ attribute.setPosition(true);
+ attribute.setFastSearch(true);
+ field.addAttribute(attribute);
+
+ ScriptExpression script = inputField.getIndexingScript();
+ script = (ScriptExpression)new RemoveSummary(inputField.getName()).convert(script);
+ script = (ScriptExpression)new PerformZCurve(field, fieldName).convert(script);
+ field.setIndexingScript(script);
+ return field;
+ }
+
+ private void ensureCompatibleSummary(SDField field, String sourceName, String summaryName, DataType summaryType,
+ SummaryTransform summaryTransform, Collection<String> summaryTo) {
+ SummaryField summary = search.getSummaryField(summaryName);
+ if (summary == null) {
+ summary = new SummaryField(summaryName, summaryType, summaryTransform);
+ summary.addDestination("default");
+ summary.addDestinations(summaryTo);
+ field.addSummaryField(summary);
+ } else if (!summary.getDataType().equals(summaryType)) {
+ fail(search, field, "Incompatible summary field '" + summaryName + "' type "+summary.getDataType()+" already created.");
+ } else if (summary.getTransform() == SummaryTransform.NONE) {
+ summary.setTransform(summaryTransform);
+ summary.addDestination("default");
+ summary.addDestinations(summaryTo);
+ } else if (summary.getTransform() != summaryTransform) {
+ deployLogger.log(Level.WARNING, "Summary field " + summaryName + " has wrong transform: " + summary.getTransform());
+ return;
+ }
+ SummaryField.Source source = new SummaryField.Source(sourceName);
+ summary.getSources().clear();
+ summary.addSource(source);
+ }
+
+ private Set<String> removeSummaryTo(SDField field) {
+ Set<String> summaryTo = new HashSet<>();
+ Collection<SummaryField> summaryFields = field.getSummaryFields();
+ for (SummaryField summary : summaryFields) {
+ summaryTo.addAll(summary.getDestinations());
+ }
+ summaryFields.clear();
+ return summaryTo;
+ }
+
+ private static boolean isSupportedPositionType(DataType dataType) {
+ if (dataType instanceof ArrayDataType) {
+ dataType = ((ArrayDataType)dataType).getNestedType();
+ }
+ return dataType.equals(PositionDataType.INSTANCE);
+ }
+
+ private static class RemoveSummary extends ExpressionConverter {
+
+ final String find;
+
+ RemoveSummary(String find) {
+ this.find = find;
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ if (!(exp instanceof SummaryExpression)) {
+ return false;
+ }
+ String fieldName = ((SummaryExpression)exp).getFieldName();
+ return fieldName == null || fieldName.equals(find);
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ return null;
+ }
+ }
+
+ private static class PerformZCurve extends ExpressionConverter {
+
+ final String find;
+ final String replace;
+ final boolean isArray;
+
+ PerformZCurve(SDField find, String replace) {
+ this.find = find.getName();
+ this.replace = replace;
+ this.isArray = find.getDataType() instanceof ArrayDataType;
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ if (!(exp instanceof AttributeExpression)) {
+ return false;
+ }
+ String fieldName = ((AttributeExpression)exp).getFieldName();
+ return fieldName == null || fieldName.equals(find);
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ return new StatementExpression(
+ isArray ? new ForEachExpression(new ZCurveExpression()) :
+ new ZCurveExpression(), new AttributeExpression(replace));
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/DeprecateAttributePrefetch.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/DeprecateAttributePrefetch.java
new file mode 100644
index 00000000000..6d5b3ff8936
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/DeprecateAttributePrefetch.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+public class DeprecateAttributePrefetch extends Processor {
+
+ public DeprecateAttributePrefetch(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ for (Attribute a : field.getAttributes().values()) {
+ if (Boolean.TRUE.equals(a.getPrefetchValue())) {
+ warn(search, field, "Attribute prefetch is deprecated. Use an explicitly defined document summary with all desired fields defined as attribute.");
+ }
+ if (Boolean.FALSE.equals(a.getPrefetchValue())) {
+ warn(search, field, "Attribute prefetch is deprecated. no-prefetch can be removed.");
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/DisallowComplexMapAndWsetKeyTypes.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/DisallowComplexMapAndWsetKeyTypes.java
new file mode 100644
index 00000000000..dbe83143189
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/DisallowComplexMapAndWsetKeyTypes.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.document.MapDataType;
+import com.yahoo.document.PrimitiveDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.document.WeightedSetDataType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Non-primitive key types for map and weighted set forbidden (though OK in document model)
+ * @author vegardh
+ *
+ */
+public class DisallowComplexMapAndWsetKeyTypes extends Processor {
+
+ public DisallowComplexMapAndWsetKeyTypes(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ // TODO also traverse struct types to search for bad map or wset types there. Do this after document manager is fixed, do
+ // not start using the static stuff on SDDocumentTypes any more.
+ for (SDField field : search.allFieldsList()) {
+ if (field.getDataType() instanceof WeightedSetDataType) {
+ DataType nestedType = ((WeightedSetDataType)field.getDataType()).getNestedType();
+ if (!(nestedType instanceof PrimitiveDataType)) {
+ fail(search, field, "Weighted set must have a primitive key type.");
+ }
+ } else if (field.getDataType() instanceof MapDataType) {
+ if (!(((MapDataType)field.getDataType()).getKeyType() instanceof PrimitiveDataType)) {
+ fail(search, field, "Map key type must be a primitive type");
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/DiversitySettingsValidator.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/DiversitySettingsValidator.java
new file mode 100644
index 00000000000..49701f3e443
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/DiversitySettingsValidator.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Created by balder on 3/10/15.
+ */
+public class DiversitySettingsValidator extends Processor {
+ public DiversitySettingsValidator(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (RankProfile rankProfile : rankProfileRegistry.localRankProfiles(search)) {
+ if (rankProfile.getMatchPhaseSettings() != null && rankProfile.getMatchPhaseSettings().getDiversity() != null) {
+ validate(rankProfile, rankProfile.getMatchPhaseSettings().getDiversity());
+ }
+ }
+ }
+ private void validate(RankProfile rankProfile, RankProfile.DiversitySettings settings) {
+ String attributeName = settings.getAttribute();
+ new AttributeValidator(search.getName(), rankProfile.getName(),
+ search.getAttribute(attributeName), attributeName).validate();
+ }
+
+ private static class AttributeValidator extends MatchPhaseSettingsValidator.AttributeValidator {
+ public AttributeValidator(String searchName, String rankProfileName, Attribute attribute, String attributeName) {
+ super(searchName, rankProfileName, attribute, attributeName);
+ }
+
+ protected void validateThatAttributeIsSingleAndNotPredicate() {
+ if (!attribute.getCollectionType().equals(Attribute.CollectionType.SINGLE) ||
+ attribute.getType().equals(Attribute.Type.PREDICATE))
+ {
+ failValidation("must be single value numeric, or enumerated attribute, but it is '"
+ + attribute.getDataType().getName() + "'");
+ }
+ }
+
+ @Override
+ public void validate() {
+ validateThatAttributeExists();
+ validateThatAttributeIsSingleAndNotPredicate();
+ }
+
+ @Override
+ public String getValidationType() {
+ return "diversity";
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/ExactMatch.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ExactMatch.java
new file mode 100644
index 00000000000..d53be4bd18d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ExactMatch.java
@@ -0,0 +1,98 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.vespa.indexinglanguage.ExpressionSearcher;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * The implementation of exact matching
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ExactMatch extends Processor {
+
+ public static final String DEFAULT_EXACT_TERMINATOR = "@@";
+
+ public ExactMatch(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ Matching.Type matching = field.getMatching().getType();
+ if (matching.equals(Matching.Type.EXACT) ||
+ matching.equals(Matching.Type.WORD))
+ {
+ implementExactMatch(field, search);
+ } else if (field.getMatching().getExactMatchTerminator() != null) {
+ warn(search, field, "exact-terminator requires 'exact' matching to have any effect.");
+ }
+ }
+ }
+
+ private void implementExactMatch(SDField field, Search search) {
+ field.setStemming(Stemming.NONE);
+ field.getNormalizing().inferLowercase();
+
+ if (field.getMatching().getType().equals(Matching.Type.WORD)) {
+ field.addQueryCommand("word");
+ } else { // exact
+ String exactTerminator = DEFAULT_EXACT_TERMINATOR;
+ if (field.getMatching().getExactMatchTerminator() != null &&
+ ! field.getMatching().getExactMatchTerminator().equals(""))
+ {
+ exactTerminator = field.getMatching().getExactMatchTerminator();
+ } else {
+ warn(search, field,
+ "With 'exact' matching, an exact-terminator is needed (using \""
+ + exactTerminator +"\" as terminator)");
+ }
+ field.addQueryCommand("exact " + exactTerminator);
+
+ // The following part illustrates how nice it would have been with canonical
+ // representation of indices
+ if (field.doesIndexing()) {
+ exactMatchSettingsForField(field);
+ }
+ }
+ ScriptExpression script = field.getIndexingScript();
+ if (new ExpressionSearcher<>(IndexExpression.class).containedIn(script)) {
+ field.setIndexingScript((ScriptExpression)new MyProvider(search).convert(field.getIndexingScript()));
+ }
+ }
+
+ private void exactMatchSettingsForField(SDField field) {
+ field.getRanking().setFilter(true);
+ }
+
+ private static class MyProvider extends TypedTransformProvider {
+
+ MyProvider(Search search) {
+ super(ExactExpression.class, search);
+ }
+
+ @Override
+ protected boolean requiresTransform(Expression exp, DataType fieldType) {
+ return exp instanceof OutputExpression;
+ }
+
+ @Override
+ protected Expression newTransform(DataType fieldType) {
+ Expression exp = new ExactExpression();
+ if (fieldType instanceof CollectionDataType) {
+ exp = new ForEachExpression(exp);
+ }
+ return exp;
+ }
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/FieldSetValidity.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/FieldSetValidity.java
new file mode 100644
index 00000000000..e809fe90c7f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/FieldSetValidity.java
@@ -0,0 +1,94 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.FieldSet;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.NormalizeLevel;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Computes the right "index commands" for each fieldset in a search definition.
+ *
+ *
+ * @author vegardh
+ * @author bratseth
+ */
+public class FieldSetValidity extends Processor {
+
+ public FieldSetValidity(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (FieldSet fieldSet : search.fieldSets().userFieldSets().values()) {
+ checkFieldNames(search, fieldSet);
+ checkMatching(search, fieldSet);
+ checkNormalization(search, fieldSet);
+ checkStemming(search, fieldSet);
+ }
+ }
+
+ private void checkFieldNames(Search search, FieldSet fieldSet) {
+ for (String fld : fieldSet.getFieldNames()) {
+ SDField field = search.getField(fld);
+ if (field == null) {
+ throw new IllegalArgumentException("For search '" + search.getName() + "': Field '"+ fld + "' in " + fieldSet + " does not exist.");
+ }
+ }
+ }
+
+ private void checkMatching(Search search, FieldSet fieldSet) {
+ Matching fsMatching = null;
+ for (String fld : fieldSet.getFieldNames()) {
+ SDField field = search.getField(fld);
+ Matching fieldMatching = field.getMatching();
+ if (fsMatching==null) {
+ fsMatching = fieldMatching;
+ } else {
+ if (fsMatching!=null && !fsMatching.equals(fieldMatching)) {
+ warn(search, field, "The matching settings for the fields in " + fieldSet + " are inconsistent (explicitly or because of field type). This may lead to recall and ranking issues.");
+ return;
+ }
+ }
+ }
+ }
+
+ private void checkNormalization(Search search, FieldSet fieldSet) {
+ NormalizeLevel.Level fsNorm = null;
+ for (String fld : fieldSet.getFieldNames()) {
+ SDField field = search.getField(fld);
+ NormalizeLevel.Level fieldNorm = field.getNormalizing().getLevel();
+ if (fsNorm==null) {
+ fsNorm = fieldNorm;
+ } else {
+ if (fsNorm!=null && !fsNorm.equals(fieldNorm)) {
+ warn(search, field, "The normalization settings for the fields in " + fieldSet + " are inconsistent (explicitly or because of field type). This may lead to recall and ranking issues.");
+ return;
+ }
+ }
+ }
+ }
+
+ private void checkStemming(Search search, FieldSet fieldSet) {
+ Stemming fsStemming = null;
+ for (String fld : fieldSet.getFieldNames()) {
+ SDField field = search.getField(fld);
+ Stemming fieldStemming = field.getStemming();
+ if (fsStemming==null) {
+ fsStemming = fieldStemming;
+ } else {
+ if (fsStemming!=null && !fsStemming.equals(fieldStemming)) {
+ warn(search, field, "The stemming settings for the fields in the fieldset '"+fieldSet.getName()+"' are inconsistent (explicitly or because of field type). This may lead to recall and ranking issues.");
+ return;
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/FilterFieldNames.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/FilterFieldNames.java
new file mode 100644
index 00000000000..1df8c642750
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/FilterFieldNames.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.logging.Level;
+
+/**
+ * Takes the fields and indexes that are of type rank filter, and stores those names on all rank profiles
+ * @author vegardh
+ *
+ */
+public class FilterFieldNames extends Processor {
+
+ public FilterFieldNames(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField f : search.allFieldsList()) {
+ if (f.getRanking().isFilter()) {
+ filterField(f.getName());
+ }
+ }
+ for (RankProfile profile : rankProfileRegistry.localRankProfiles(search)) {
+ Set<String> filterFields = new LinkedHashSet<>();
+ findFilterFields(search, profile, filterFields);
+ for (Iterator<String> itr = filterFields.iterator(); itr.hasNext(); ) {
+ String fieldName = itr.next();
+ profile.filterFields().add(fieldName);
+ profile.addRankSetting(fieldName, RankProfile.RankSetting.Type.RANKTYPE, RankType.EMPTY);
+ }
+ }
+ }
+
+ private void filterField(String f) {
+ for (RankProfile rp : rankProfileRegistry.localRankProfiles(search)) {
+ rp.filterFields().add(f);
+ }
+ }
+
+ private void findFilterFields(Search search, RankProfile profile, Set<String> filterFields) {
+ for (Iterator<RankProfile.RankSetting> itr = profile.declaredRankSettingIterator(); itr.hasNext(); ) {
+ RankProfile.RankSetting setting = itr.next();
+ if (setting.getType().equals(RankProfile.RankSetting.Type.PREFERBITVECTOR) &&
+ ((Boolean)setting.getValue()).booleanValue())
+ {
+ String fieldName = setting.getFieldName();
+ if (search.getField(fieldName) != null) {
+ if (!profile.filterFields().contains(fieldName)) {
+ filterFields.add(fieldName);
+ }
+ } else {
+ deployLogger.log(Level.WARNING, "For rank profile '" + profile.getName() + "': Cannot apply rank filter setting to unexisting field '" + fieldName + "'");
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaries.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaries.java
new file mode 100644
index 00000000000..fa7725843b1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaries.java
@@ -0,0 +1,222 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import java.util.logging.Level;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Makes implicitly defined summaries into explicit summaries
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ImplicitSummaries extends Processor {
+
+ public ImplicitSummaries(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ DocumentSummary defaultSummary=search.getSummary("default");
+ if (defaultSummary==null) {
+ defaultSummary=new DocumentSummary("default");
+ search.addSummary(defaultSummary);
+ }
+
+ for (SDField field : search.allFieldsList()) {
+ collectSummaries(field,search);
+ }
+
+ for (DocumentSummary documentSummary : search.getSummaries().values()) {
+ documentSummary.purgeImplicits();
+ }
+ }
+
+ private void addSummaryFieldSources(SummaryField summaryField, SDField sdField) {
+ sdField.addSummaryFieldSources(summaryField);
+ }
+
+ private void collectSummaries(SDField field,Search search) {
+ SummaryField addedSummaryField=null;
+
+ // Implicit
+ String fieldName = field.getName();
+ SummaryField fieldSummaryField=field.getSummaryField(fieldName);
+ if (fieldSummaryField == null && field.doesSummarying()) {
+ fieldSummaryField=new SummaryField(fieldName, field.getDataType());
+ fieldSummaryField.setImplicit(true);
+ addSummaryFieldSources(fieldSummaryField, field);
+ fieldSummaryField.addDestination("default");
+ field.addSummaryField(fieldSummaryField);
+ addedSummaryField = fieldSummaryField;
+ }
+ if (fieldSummaryField != null) {
+ for (String dest : fieldSummaryField.getDestinations()) {
+ DocumentSummary summary = search.getSummary(dest);
+ if (summary != null) {
+ summary.add(fieldSummaryField);
+ }
+ }
+ }
+
+ // Attribute prefetch
+ for (Attribute attribute : field.getAttributes().values()) {
+ if (attribute.getName().equals(fieldName)) {
+ if (addedSummaryField != null) {
+ addedSummaryField.setTransform(SummaryTransform.ATTRIBUTE);
+ }
+ if (attribute.isPrefetch()) {
+ addPrefetchAttribute(attribute, field, search);
+ }
+ }
+ }
+
+ // Position attributes
+ if (field.doesSummarying()) {
+ for (Attribute attribute : field.getAttributes().values()) {
+ if (!attribute.isPosition()) {
+ continue;
+ }
+ DocumentSummary attributePrefetchSummary = getOrCreateAttributePrefetchSummary(search);
+ attributePrefetchSummary.add(field.getSummaryField(PositionDataType.getDistanceSummaryFieldName(fieldName)));
+ attributePrefetchSummary.add(field.getSummaryField(PositionDataType.getPositionSummaryFieldName(fieldName)));
+ }
+ }
+
+ // Explicits
+ for (SummaryField summaryField : field.getSummaryFields()) {
+ // Make sure we fetch from attribute here too
+ Attribute attribute=field.getAttributes().get(fieldName);
+ if (attribute!=null && summaryField.getTransform()==SummaryTransform.NONE) {
+ summaryField.setTransform(SummaryTransform.ATTRIBUTE);
+ }
+
+ if (isValid(summaryField,search)) {
+ addToDestinations(summaryField, search);
+ }
+ }
+
+ }
+
+ private DocumentSummary getOrCreateAttributePrefetchSummary(Search search) {
+ DocumentSummary summary = search.getSummary("attributeprefetch");
+ if (summary == null) {
+ summary = new DocumentSummary("attributeprefetch");
+ search.addSummary(summary);
+ }
+ return summary;
+ }
+
+
+ private void addPrefetchAttribute(Attribute attribute,SDField field,Search search) {
+ if (attribute.getPrefetchValue()==null) { // Prefetch by default - unless any summary makes this dynamic
+ // Check if there is an implicit dynamic definition
+ SummaryField fieldSummaryField=field.getSummaryField(attribute.getName());
+ if (fieldSummaryField!=null && fieldSummaryField.getTransform().isDynamic()) return;
+
+ // Check if an explicit class makes it dynamic (first is enough, as all must be the same, checked later)
+ SummaryField explicitSummaryField=search.getExplicitSummaryField(attribute.getName());
+ if (explicitSummaryField!=null && explicitSummaryField.getTransform().isDynamic()) return;
+ }
+
+ DocumentSummary summary = getOrCreateAttributePrefetchSummary(search);
+ SummaryField attributeSummaryField = new SummaryField(attribute.getName(), attribute.getDataType());
+ attributeSummaryField.addSource(attribute.getName());
+ attributeSummaryField.addDestination("attributeprefetch");
+ attributeSummaryField.setTransform(SummaryTransform.ATTRIBUTE);
+ summary.add(attributeSummaryField);
+ }
+
+ // Returns whether this is valid. Warns if invalid and ignorable. Throws if not ignorable.
+ private boolean isValid(SummaryField summaryField,Search search) {
+ if (summaryField.getTransform() == SummaryTransform.DISTANCE ||
+ summaryField.getTransform() == SummaryTransform.POSITIONS)
+ {
+ int sourceCount = summaryField.getSourceCount();
+ if (sourceCount != 1) {
+ throw newProcessException(search.getName(), summaryField.getName(),
+ "Expected 1 source field, got " + sourceCount + ".");
+ }
+ String sourceName = summaryField.getSingleSource();
+ if (search.getAttribute(sourceName) == null) {
+ throw newProcessException(search.getName(), summaryField.getName(),
+ "Summary source attribute '" + sourceName + "' not found.");
+ }
+ return true;
+ }
+
+ String fieldName = summaryField.getSourceField();
+ SDField sourceField = search.getField(fieldName);
+ if (sourceField == null) {
+ throw newProcessException(search, summaryField, "Source field '" + fieldName + "' does not exist.");
+ }
+ if (!sourceField.doesSummarying() &&
+ !summaryField.getTransform().equals(SummaryTransform.ATTRIBUTE) &&
+ !summaryField.getTransform().equals(SummaryTransform.GEOPOS))
+ {
+ // Summary transform attribute may indicate that the ilscript was rewritten to remove summary
+ // by another search that uses this same field in inheritance.
+ deployLogger.log(Level.WARNING, "Ignoring " + summaryField + ": " + sourceField +
+ " is not creating a summary value in its indexing statement");
+ return false;
+ }
+
+ if (summaryField.getTransform().isDynamic()
+ && summaryField.getName().equals(sourceField.getName())
+ && sourceField.doesAttributing()) {
+ Attribute attribute=sourceField.getAttributes().get(sourceField.getName());
+ if (attribute!=null) {
+ String destinations="document summary 'default'";
+ if (summaryField.getDestinations().size()>0) {
+ destinations = "document summaries " + summaryField.getDestinations();
+ }
+ deployLogger.log(Level.WARNING, "Will fetch the disk summary value of " + sourceField + " in " + destinations +
+ " since this summary field uses a dynamic summary value (snippet/bolding): Dynamic summaries and bolding " +
+ "is not supported with summary values fetched from in-memory attributes yet. If you want to see partial updates " +
+ "to this attribute, remove any bolding and dynamic snippeting from this field");
+ // Note: The dynamic setting has already overridden the attribute map setting,
+ // so we do not need to actually do attribute.setSummary(false) here
+ // Also, we can not do this, since it makes it impossible to fetch this attribute
+ // in another summary
+ }
+ }
+
+ return true;
+ }
+
+ private void addToDestinations(SummaryField summaryField,Search search) {
+ if (summaryField.getDestinations().size()==0) {
+ addToDestination("default",summaryField,search);
+ }
+ else {
+ for (String destinationName : summaryField.getDestinations())
+ addToDestination(destinationName,summaryField,search);
+ }
+ }
+
+ private void addToDestination(String destinationName,SummaryField summaryField,Search search) {
+ DocumentSummary destination=search.getSummary(destinationName);
+ if (destination==null) {
+ destination=new DocumentSummary(destinationName);
+ search.addSummary(destination);
+ destination.add(summaryField);
+ }
+ else {
+ SummaryField existingField=
+ destination.getSummaryField(summaryField.getName());
+ SummaryField merged=summaryField.mergeWith(existingField);
+ destination.add(merged);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFields.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFields.java
new file mode 100644
index 00000000000..b95f2469cd8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFields.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * This processor adds all implicit summary fields to all registered document summaries. If another field has already
+ * been registered with one of the implicit names, this processor will throw an {@link IllegalStateException}.
+ */
+public class ImplicitSummaryFields extends Processor {
+
+ public ImplicitSummaryFields(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (DocumentSummary docsum : search.getSummaries().values()) {
+ addField(docsum, new SummaryField("rankfeatures", DataType.STRING, SummaryTransform.RANKFEATURES));
+ addField(docsum, new SummaryField("summaryfeatures", DataType.STRING, SummaryTransform.SUMMARYFEATURES));
+ }
+ }
+
+ private void addField(DocumentSummary docsum, SummaryField field) {
+ if (docsum.getSummaryField(field.getName()) != null) {
+ throw new IllegalStateException("Summary class '" + docsum.getName() + "' uses reserved field name '" +
+ field.getName() + "'.");
+ }
+ docsum.add(field);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexFieldNames.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexFieldNames.java
new file mode 100644
index 00000000000..2c18c58d3dc
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexFieldNames.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Because of the way the parser works (allowing any token as identifier),
+ * it is not practical to limit the syntax of field names there, do it here.
+ * Important to disallow dash, has semantic in IL.
+ * @author vegardh
+ *
+ */
+public class IndexFieldNames extends Processor {
+
+ private static final String FIELD_NAME_REGEXP = "[a-zA-Z]\\w*";
+
+ public IndexFieldNames(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (!field.getName().matches(FIELD_NAME_REGEXP) && !legalDottedPositionField(field)) {
+ fail(search, field, " Not a legal field name. Legal expression: " + FIELD_NAME_REGEXP);
+ }
+ }
+ }
+
+ /**
+ * In {@link CreatePositionZCurve} we add some .position and .distance fields for pos fields. Make an exception for those for now. For 6.0, rename
+ * to _position and _distance and delete this method.
+ * @param field an {@link com.yahoo.searchdefinition.document.SDField}
+ * @return true if allowed
+ */
+ private boolean legalDottedPositionField(SDField field) {
+ return field.getName().endsWith(".position") || field.getName().endsWith(".distance");
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexSettingsNonFieldNames.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexSettingsNonFieldNames.java
new file mode 100644
index 00000000000..fecc5e2309d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexSettingsNonFieldNames.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Iterator;
+
+/**
+ * Fail if:
+ * 1) There are index: settings without explicit index names (name same as field name)
+ * 2) All the index-to indexes differ from the field name.
+ * @author vegardh
+ *
+ */
+public class IndexSettingsNonFieldNames extends Processor {
+
+ public IndexSettingsNonFieldNames(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ boolean fieldNameUsed = false;
+ for (Iterator i = field.getFieldNameAsIterator(); i.hasNext();) {
+ String iName = (String)(i.next());
+ if (iName.equals(field.getName())) {
+ fieldNameUsed = true;
+ }
+ }
+ if (!fieldNameUsed) {
+ for (Index index : field.getIndices().values()) {
+ if (index.getName().equals(field.getName())) {
+ throw new IllegalArgumentException("Error in " + field + " in " + search +
+ ": When all index names differ from field name, index parameter settings must specify index name explicitly.");
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexTo2FieldSet.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexTo2FieldSet.java
new file mode 100644
index 00000000000..3148273ddc1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexTo2FieldSet.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * @author balder
+ */
+public class IndexTo2FieldSet extends Processor {
+
+ public IndexTo2FieldSet(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ processField(field);
+ }
+ }
+ private void processField(SDField field) {
+ if (field.usesStructOrMap()) {
+ for (SDField subField : field.getStructFields()) {
+ processField(subField);
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingInputs.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingInputs.java
new file mode 100644
index 00000000000..c2e889f7993
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingInputs.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.ExpressionVisitor;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.expressions.InputExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.StatementExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * <p>This processor modifies all indexing scripts so that they input the value of the owning field by default. It also
+ * ensures that all fields used as input exist.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class IndexingInputs extends Processor {
+
+ public IndexingInputs(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ ScriptExpression script = field.getIndexingScript();
+ if (script == null) {
+ continue;
+ }
+ String fieldName = field.getName();
+ script = (ScriptExpression)new DefaultToCurrentField(fieldName).convert(script);
+ script = (ScriptExpression)new EnsureInputExpression(fieldName).convert(script);
+ new VerifyInputExpression(search, field).visit(script);
+ field.setIndexingScript(script);
+ }
+ }
+
+ private static class DefaultToCurrentField extends ExpressionConverter {
+
+ final String fieldName;
+
+ DefaultToCurrentField(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ return exp instanceof InputExpression && ((InputExpression)exp).getFieldName() == null;
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ return new InputExpression(fieldName);
+ }
+ }
+
+ private static class EnsureInputExpression extends ExpressionConverter {
+
+ final String fieldName;
+
+ EnsureInputExpression(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ return exp instanceof StatementExpression;
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ if (exp.requiredInputType() != null) {
+ return new StatementExpression(new InputExpression(fieldName), exp);
+ } else {
+ return exp;
+ }
+ }
+ }
+
+ private class VerifyInputExpression extends ExpressionVisitor {
+
+ private final Search search;
+ private final SDField field;
+
+ public VerifyInputExpression(Search search, SDField field) {
+ this.search = search;
+ this.field = field;
+ }
+
+ @Override
+ protected void doVisit(Expression exp) {
+ if (!(exp instanceof InputExpression)) {
+ return;
+ }
+ SDDocumentType docType = search.getDocument();
+ String inputField = ((InputExpression)exp).getFieldName();
+ if (docType.getField(inputField) != null) {
+ return;
+ }
+ fail(search, field, "Indexing script refers to field '" + inputField + "' which does not exist " +
+ "in document type '" + docType.getName() + "'.");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingOutputs.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingOutputs.java
new file mode 100644
index 00000000000..55e7dabf9ba
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingOutputs.java
@@ -0,0 +1,141 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.*;
+
+/**
+ * <p>This processor modifies all indexing scripts so that they output to the owning field by default. It also prevents
+ * any output expression from writing to any field except for the owning field. Finally, for <tt>SummaryExpression</tt>,
+ * this processor expands to write all appropriate summary fields.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class IndexingOutputs extends Processor {
+
+ public IndexingOutputs(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ ScriptExpression script = field.getIndexingScript();
+ if (script == null) {
+ continue;
+ }
+ Set<String> summaryFields = new TreeSet<>();
+ findSummaryTo(search, field, summaryFields, summaryFields);
+ MyConverter converter = new MyConverter(search, field, summaryFields);
+ field.setIndexingScript((ScriptExpression)converter.convert(script));
+ }
+ }
+
+ public void findSummaryTo(Search search, SDField field, Set<String> dynamicSummary,
+ Set<String> staticSummary) {
+ Map<String, SummaryField> summaryFields = search.getSummaryFields(field);
+ if (summaryFields.isEmpty()) {
+ fillSummaryToFromField(field, dynamicSummary, staticSummary);
+ } else {
+ fillSummaryToFromSearch(search, field, summaryFields, dynamicSummary, staticSummary);
+ }
+ }
+
+ private void fillSummaryToFromSearch(Search search, SDField field, Map<String, SummaryField> summaryFields,
+ Set<String> dynamicSummary, Set<String> staticSummary) {
+ for (SummaryField summaryField : summaryFields.values()) {
+ fillSummaryToFromSummaryField(search, field, summaryField, dynamicSummary, staticSummary);
+ }
+ }
+
+ private void fillSummaryToFromSummaryField(Search search, SDField field, SummaryField summaryField, Set<String> dynamicSummary, Set<String> staticSummary) {
+ SummaryTransform summaryTransform = summaryField.getTransform();
+ String summaryName = summaryField.getName();
+ if (summaryTransform.isDynamic() && summaryField.getSourceCount() > 2) {
+ // Avoid writing to summary fields that have more than a single input field, as that is handled by the
+ // summary rewriter in the search core.
+ return;
+ }
+ if (summaryTransform.isDynamic()) {
+ DataType fieldType = field.getDataType();
+ if (fieldType != DataType.URI && fieldType != DataType.STRING) {
+ warn(search, field, "Dynamic summaries are only supported for fields of type " +
+ "string, ignoring summary field '" + summaryField.getName() +
+ "' for sd field '" + field.getName() + "' of type " +
+ fieldType.getName() + ".");
+ return;
+ }
+ dynamicSummary.add(summaryName);
+ } else if (summaryTransform != SummaryTransform.ATTRIBUTE) {
+ staticSummary.add(summaryName);
+ }
+ }
+
+ private static void fillSummaryToFromField(SDField field, Set<String> dynamicSummary, Set<String> staticSummary) {
+ for (SummaryField summaryField : field.getSummaryFields()) {
+ String summaryName = summaryField.getName();
+ if (summaryField.getTransform().isDynamic()) {
+ dynamicSummary.add(summaryName);
+ } else {
+ staticSummary.add(summaryName);
+ }
+ }
+ }
+
+ private class MyConverter extends ExpressionConverter {
+
+ final Search search;
+ final Field field;
+ final Set<String> summaryFields;
+
+ MyConverter(Search search, Field field, Set<String> summaryFields) {
+ this.search = search;
+ this.field = field;
+ this.summaryFields = summaryFields.isEmpty() ? Collections.singleton(field.getName()) : summaryFields;
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ if (!(exp instanceof OutputExpression)) {
+ return false;
+ }
+ String fieldName = ((OutputExpression)exp).getFieldName();
+ if (fieldName == null) {
+ return true; // inject appropriate field name
+ }
+ if (!fieldName.equals(field.getName())) {
+ fail(search, field, "Indexing expression '" + exp + "' attempts to write to a field other than '" +
+ field.getName() + "'.");
+ }
+ return false;
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ List<Expression> ret = new LinkedList<>();
+ if (exp instanceof AttributeExpression) {
+ ret.add(new AttributeExpression(field.getName()));
+ } else if (exp instanceof IndexExpression) {
+ ret.add(new IndexExpression(field.getName()));
+ } else if (exp instanceof SummaryExpression) {
+ for (String fieldName : summaryFields) {
+ ret.add(new SummaryExpression(fieldName));
+ }
+ } else {
+ throw new UnsupportedOperationException(exp.getClass().getName());
+ }
+ return new StatementExpression(ret);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValidation.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValidation.java
new file mode 100644
index 00000000000..af04deb5347
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValidation.java
@@ -0,0 +1,149 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class IndexingValidation extends Processor {
+
+ public IndexingValidation(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ VerificationContext ctx = new VerificationContext(new MyAdapter(search));
+ for (SDField field : search.allFieldsList()) {
+ ScriptExpression script = field.getIndexingScript();
+ try {
+ script.verify(ctx);
+ MyConverter converter = new MyConverter();
+ for (StatementExpression exp : script) {
+ converter.convert(exp); // TODO: stop doing this explicitly when visiting a script does not branch
+ }
+ } catch (VerificationException e) {
+ fail(search, field, "For expression '" + e.getExpression() + "': " + e.getMessage());
+ }
+ }
+ }
+
+ private static class MyConverter extends ExpressionConverter {
+
+ final Set<String> outputs = new HashSet<>();
+ final Set<String> prevNames = new HashSet<>();
+
+ @Override
+ protected ExpressionConverter branch() {
+ MyConverter ret = new MyConverter();
+ ret.outputs.addAll(outputs);
+ ret.prevNames.addAll(prevNames);
+ return ret;
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ if (exp instanceof OutputExpression) {
+ String fieldName = ((OutputExpression)exp).getFieldName();
+ if (outputs.contains(fieldName) && !prevNames.contains(fieldName)) {
+ throw new VerificationException(exp, "Attempting to assign conflicting values to field '" +
+ fieldName + "'.");
+ }
+ outputs.add(fieldName);
+ prevNames.add(fieldName);
+ }
+ if (exp.createdOutputType() != null) {
+ prevNames.clear();
+ }
+ return false;
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private static class MyAdapter implements FieldTypeAdapter {
+
+ final Search search;
+
+ public MyAdapter(Search search) {
+ this.search = search;
+ }
+
+ @Override
+ public DataType getInputType(Expression exp, String fieldName) {
+ SDField field = search.getDocumentField(fieldName);
+ if (field == null) {
+ throw new VerificationException(exp, "Input field '" + fieldName + "' not found.");
+ }
+ return field.getDataType();
+ }
+
+ @Override
+ public void tryOutputType(Expression exp, String fieldName, DataType valueType) {
+ String fieldDesc;
+ DataType fieldType;
+ if (exp instanceof AttributeExpression) {
+ Attribute attribute = search.getAttribute(fieldName);
+ if (attribute == null) {
+ throw new VerificationException(exp, "Attribute '" + fieldName + "' not found.");
+ }
+ fieldDesc = "attribute";
+ fieldType = attribute.getDataType();
+ } else if (exp instanceof IndexExpression) {
+ SDField field = search.getField(fieldName);
+ if (field == null) {
+ throw new VerificationException(exp, "Index field '" + fieldName + "' not found.");
+ }
+ fieldDesc = "index field";
+ fieldType = field.getDataType();
+ } else if (exp instanceof SummaryExpression) {
+ SummaryField field = search.getSummaryField(fieldName);
+ if (field == null) {
+ throw new VerificationException(exp, "Summary field '" + fieldName + "' not found.");
+ }
+ fieldDesc = "summary field";
+ fieldType = field.getDataType();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ if (!fieldType.isAssignableFrom(valueType) &&
+ !fieldType.isAssignableFrom(createCompatType(valueType)))
+ {
+ throw new VerificationException(exp, "Can not assign " + valueType.getName() + " to " + fieldDesc +
+ " '" + fieldName + "' which is " + fieldType.getName() + ".");
+ }
+ }
+
+ private static DataType createCompatType(DataType origType) {
+ if (origType instanceof ArrayDataType) {
+ return DataType.getArray(createCompatType(((ArrayDataType)origType).getNestedType()));
+ } else if (origType instanceof MapDataType) {
+ MapDataType mapType = (MapDataType)origType;
+ return DataType.getMap(createCompatType(mapType.getKeyType()),
+ createCompatType(mapType.getValueType()));
+ } else if (origType instanceof WeightedSetDataType) {
+ return DataType.getWeightedSet(createCompatType(((WeightedSetDataType)origType).getNestedType()));
+ } else if (origType == PositionDataType.INSTANCE) {
+ return DataType.LONG;
+ } else {
+ return origType;
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValues.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValues.java
new file mode 100644
index 00000000000..ff8be71f8c3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IndexingValues.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.expressions.InputExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.OutputExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class IndexingValues extends Processor {
+
+ public IndexingValues(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (Field field : search.getDocument().fieldSet()) {
+ SDField sdField = (SDField)field;
+ if (!sdField.isExtraField()) {
+ new RequireThatDocumentFieldsAreImmutable(field).convert(sdField.getIndexingScript());
+ }
+ }
+ }
+
+ private class RequireThatDocumentFieldsAreImmutable extends ExpressionConverter {
+
+ final Field field;
+ Expression mutatedBy;
+
+ RequireThatDocumentFieldsAreImmutable(Field field) {
+ this.field = field;
+ }
+
+ @Override
+ public ExpressionConverter branch() {
+ return clone();
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ if (exp instanceof OutputExpression && mutatedBy != null) {
+ throw newProcessException(search, field,
+ "Indexing expression '" + mutatedBy + "' modifies the value of the " +
+ "document field '" + field.getName() + "'. This is no longer supported -- " +
+ "declare such fields outside the document.");
+ }
+ if (exp instanceof InputExpression && ((InputExpression)exp).getFieldName().equals(field.getName())) {
+ mutatedBy = null;
+ } else if (exp.createdOutputType() != null) {
+ mutatedBy = exp;
+ }
+ return false;
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/IntegerIndex2Attribute.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IntegerIndex2Attribute.java
new file mode 100644
index 00000000000..a29896ce3e8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/IntegerIndex2Attribute.java
@@ -0,0 +1,86 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.NumericDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.ExpressionVisitor;
+import com.yahoo.vespa.indexinglanguage.expressions.AttributeExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.expressions.IndexExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Replaces the 'index' statement of all numerical fields to 'attribute' because we no longer support numerical indexes.
+ *
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class IntegerIndex2Attribute extends Processor {
+
+ public IntegerIndex2Attribute(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.doesIndexing() && field.getDataType().getPrimitiveType() instanceof NumericDataType) {
+ // Avoid changing for example RISE fields
+ if (field.getIndex(field.getName()) != null && !(field.getIndex(field.getName()).getType().equals(Index.Type.VESPA))) continue;
+ ScriptExpression script = field.getIndexingScript();
+ Set<String> attributeNames = new HashSet<>();
+ new MyVisitor(attributeNames).visit(script);
+ field.setIndexingScript((ScriptExpression)new MyConverter(attributeNames).convert(script));
+ warn(search, field, "Changed to attribute because numerical indexes (field has type "+field.getDataType().getName()+") is not currently supported." +
+ " Index-only settings may fail. Ignore this warning for streaming search.");
+ }
+ }
+ }
+
+ private static class MyVisitor extends ExpressionVisitor {
+
+ final Set<String> attributeNames;
+
+ public MyVisitor(Set<String> attributeNames) {
+ this.attributeNames = attributeNames;
+ }
+
+ @Override
+ protected void doVisit(Expression exp) {
+ if (exp instanceof AttributeExpression) {
+ attributeNames.add(((AttributeExpression)exp).getFieldName());
+ }
+ }
+ }
+
+ private static class MyConverter extends ExpressionConverter {
+
+ final Set<String> attributeNames;
+
+ public MyConverter(Set<String> attributeNames) {
+ this.attributeNames = attributeNames;
+ }
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ return exp instanceof IndexExpression;
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ String indexName = ((IndexExpression)exp).getFieldName();
+ if (attributeNames.contains(indexName)) {
+ return null;
+ }
+ return new AttributeExpression(indexName);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/LiteralBoost.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/LiteralBoost.java
new file mode 100644
index 00000000000..e4e96bb51ab
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/LiteralBoost.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Iterator;
+
+/**
+ * Expresses literal boosts in terms of extra indices with rank boost.
+ * One extra index named <i>indexname</i>_exact is added for each index having
+ * a fields with literal-boosts of zero or more (zero to support other
+ * rank profiles setting a literal boost). Complete boost values in to fields
+ * are translated to rank boosts to the implementation indices.
+ * These indices has no positional
+ * or phrase support and contains concatenated versions of each field value
+ * of complete-boosted fields indexed to <i>indexname</i>. A search for indexname
+ * will be rewritten to also search <i>indexname</i>_exaxt
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class LiteralBoost extends Processor {
+
+ public LiteralBoost(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ /** Adds extra search fields and indices to express literal boosts */
+ @Override
+ public void process() {
+ checkRankModifierRankType(search);
+ addLiteralBoostsToFields(search);
+ reduceFieldLiteralBoosts(search);
+ }
+
+ /** Checks if literal boost is given using rank: , and set the actual literal boost accordingly. */
+ private void checkRankModifierRankType(Search search) {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getLiteralBoost() > -1) continue; // Let explicit value take precedence
+ if (field.getRanking().isLiteral())
+ field.setLiteralBoost(100);
+ }
+ }
+
+ /**
+ * Ensures there are field boosts for all literal boosts mentioned in rank profiles.
+ * This is required because boost indices will only be generated by looking
+ * at field boosts
+ */
+ private void addLiteralBoostsToFields(Search search) {
+ Iterator i = matchingRankSettingsIterator(search, RankProfile.RankSetting.Type.LITERALBOOST);
+ while (i.hasNext()) {
+ RankProfile.RankSetting setting = (RankProfile.RankSetting)i.next();
+ SDField field = search.getField(setting.getFieldName());
+ if (field == null) continue;
+ if (field.getLiteralBoost() < 0)
+ field.setLiteralBoost(0);
+ }
+ }
+
+ private void reduceFieldLiteralBoosts(Search search) {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getLiteralBoost()<0) continue;
+ reduceFieldLiteralBoost(field,search);
+ }
+ }
+
+ private void reduceFieldLiteralBoost(SDField field,Search search) {
+ SDField literalField = addField(search, field, "literal",
+ "{ input " + field.getName() + " | tokenize | index " + field.getName() + "_literal; }",
+ "literal-boost");
+ literalField.setWeight(field.getWeight() + field.getLiteralBoost());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeAliases.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeAliases.java
new file mode 100644
index 00000000000..856b0cf3348
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeAliases.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Takes the aliases set on field by parser and sets them on correct Index or Attribute
+ * @author vegardh
+ *
+ */
+public class MakeAliases extends Processor {
+
+ public MakeAliases(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ List<String> usedAliases = new ArrayList<>();
+ for (SDField field : search.allFieldsList()) {
+ for (Map.Entry<String, String> e : field.getAliasToName().entrySet()) {
+ String alias = e.getKey();
+ String name = e.getValue();
+ String errMsg = "For search '"+search.getName()+"': alias '"+alias+"' ";
+ if (search.existsIndex(alias)) {
+ throw new IllegalArgumentException(errMsg+"is illegal since it is the name of an index.");
+ }
+ if (search.getAttribute(alias)!=null) {
+ throw new IllegalArgumentException(errMsg+"is illegal since it is the name of an attribute.");
+ }
+ if (usedAliases.contains(alias)) {
+ throw new IllegalArgumentException(errMsg+"specified more than once.");
+ }
+ usedAliases.add(alias);
+
+ Index index = field.getIndex(name);
+ Attribute attribute = field.getAttributes().get(name);
+ if (index != null) {
+ index.addAlias(alias); // alias will be for index in this case, since it is the one used in a search
+ } else if (attribute != null && !field.doesIndexing()) {
+ attribute.getAliases().add(alias);
+ } else {
+ index = new Index(name);
+ index.addAlias(alias);
+ field.addIndex(index);
+ }
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeDefaultSummaryTheSuperSet.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeDefaultSummaryTheSuperSet.java
new file mode 100644
index 00000000000..a9f013daa98
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MakeDefaultSummaryTheSuperSet.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * <p>All summary fields which are not attributes
+ * must currently be present in the default summary class,
+ * since the default summary class also defines the docsum.dat format.
+ * This processor adds any missing summaries to the default summary.
+ * When that is decoupled from the actual summaries returned, this
+ * processor can be removed. Note: the StreamingSummary also takes advantage of
+ * the fact that default is the superset.</p>
+ *
+ * <p>All other summary logic should work unchanged without this processing step
+ * except that IndexStructureValidator.validateSummaryFields must be changed to
+ * consider all summaries, not just the default, i.e change to
+ * if (search.getSummaryField(expr.getFieldName()) == null)</p>
+ *
+ * <p>This must be done after other summary processors.</p>
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class MakeDefaultSummaryTheSuperSet extends Processor {
+
+ public MakeDefaultSummaryTheSuperSet(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ DocumentSummary defaultSummary=search.getSummary("default");
+ for (SummaryField summaryField : search.getUniqueNamedSummaryFields().values() ) {
+ if (defaultSummary.getSummaryField(summaryField.getName()) != null) continue;
+ if (summaryField.getTransform() == SummaryTransform.ATTRIBUTE) continue;
+
+ defaultSummary.add(summaryField.clone());
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchConsistency.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchConsistency.java
new file mode 100644
index 00000000000..f21670e6f78
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchConsistency.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.Matching.Type;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.indexinglanguage.ExpressionVisitor;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.expressions.IndexExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Warn on inconsistent match settings for any index
+ *
+ * @author vegardh
+ */
+public class MatchConsistency extends Processor {
+
+ public MatchConsistency(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ Map<String, Matching.Type> types = new HashMap<>();
+ for (SDField field : search.allFieldsList()) {
+ new MyVisitor(search, field, types).visit(field.getIndexingScript());
+ }
+ }
+
+ void checkMatching(Search search, SDField field, Map<String, Type> types, String indexTo) {
+ Type prevType = types.get(indexTo);
+ if (prevType == null) {
+ types.put(indexTo, field.getMatching().getType());
+ } else if (!field.getMatching().getType().equals(prevType)) {
+ warn(search, field, "The matching type for index '" + indexTo + "' (got " + field.getMatching().getType() +
+ ") is inconsistent with that given for the same index in a previous field (had " +
+ prevType + ").");
+ }
+ }
+
+ private class MyVisitor extends ExpressionVisitor {
+
+ final Search search;
+ final SDField field;
+ final Map<String, Type> types;
+
+ public MyVisitor(Search search, SDField field, Map<String, Type> types) {
+ this.search = search;
+ this.field = field;
+ this.types = types;
+ }
+
+ @Override
+ protected void doVisit(Expression exp) {
+ if (exp instanceof IndexExpression) {
+ checkMatching(search, field, types, ((IndexExpression)exp).getFieldName());
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidator.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidator.java
new file mode 100644
index 00000000000..efceeca0af0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidator.java
@@ -0,0 +1,92 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Validates the match phase settings for all registered rank profiles.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class MatchPhaseSettingsValidator extends Processor {
+
+ public MatchPhaseSettingsValidator(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (RankProfile rankProfile : rankProfileRegistry.localRankProfiles(search)) {
+ RankProfile.MatchPhaseSettings settings = rankProfile.getMatchPhaseSettings();
+ if (settings != null) {
+ validateMatchPhaseSettings(rankProfile, settings);
+ }
+ }
+ }
+
+ private void validateMatchPhaseSettings(RankProfile rankProfile, RankProfile.MatchPhaseSettings settings) {
+ String attributeName = settings.getAttribute();
+ new AttributeValidator(search.getName(), rankProfile.getName(),
+ search.getAttribute(attributeName), attributeName).validate();
+ }
+
+ public static class AttributeValidator {
+
+ private final String searchName;
+ private final String rankProfileName;
+ protected final Attribute attribute;
+ private final String attributeName;
+
+ public AttributeValidator(String searchName, String rankProfileName, Attribute attribute, String attributeName) {
+ this.searchName = searchName;
+ this.rankProfileName = rankProfileName;
+ this.attribute = attribute;
+ this.attributeName = attributeName;
+ }
+
+ public void validate() {
+ validateThatAttributeExists();
+ validateThatAttributeIsSingleNumeric();
+ validateThatAttributeIsFastSearch();
+ }
+
+ protected void validateThatAttributeExists() {
+ if (attribute == null) {
+ failValidation("does not exists");
+ }
+ }
+
+ protected void validateThatAttributeIsSingleNumeric() {
+ if (!attribute.getCollectionType().equals(Attribute.CollectionType.SINGLE) ||
+ attribute.getType().equals(Attribute.Type.STRING) ||
+ attribute.getType().equals(Attribute.Type.PREDICATE))
+ {
+ failValidation("must be single value numeric, but it is '"
+ + attribute.getDataType().getName() + "'");
+ }
+ }
+
+ protected void validateThatAttributeIsFastSearch() {
+ if (!attribute.isFastSearch()) {
+ failValidation("must be fast-search, but it is not");
+ }
+ }
+
+ protected void failValidation(String what) {
+ throw new IllegalArgumentException(createMessagePrefix() + what);
+ }
+
+ public String getValidationType() { return "match-phase"; }
+
+ private String createMessagePrefix() {
+ return "In search definition '" + searchName +
+ "', rank-profile '" + rankProfileName +
+ "': " + getValidationType() + " attribute '" + attributeName + "' ";
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/MultifieldIndexHarmonizer.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MultifieldIndexHarmonizer.java
new file mode 100644
index 00000000000..61a11fd3a4e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/MultifieldIndexHarmonizer.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.processing.multifieldresolver.IndexCommandResolver;
+import com.yahoo.searchdefinition.processing.multifieldresolver.RankTypeResolver;
+import com.yahoo.searchdefinition.processing.multifieldresolver.StemmingResolver;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Ensures that there are no conflicting types or field settings
+ * in multifield indices, either by changing settings or by splitting
+ * conflicting fields in multiple ones with different settings.
+ *
+ * @author bratseth
+ */
+public class MultifieldIndexHarmonizer extends Processor {
+
+ /** A map from index names to a List of fields going to that index */
+ private Map<String,List<SDField>> indexToFields=new java.util.HashMap<>();
+
+ public MultifieldIndexHarmonizer(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ populateIndexToFields(search);
+ resolveAllConflicts(search);
+ }
+
+ private void populateIndexToFields(Search search) {
+ for (SDField field : search.allFieldsList() ) {
+ if (!field.doesIndexing()) {
+ continue;
+ }
+ for (Iterator j = field.getFieldNameAsIterator(); j.hasNext();) {
+ String indexName = (String)j.next();
+ addIndexField(indexName, field);
+ }
+ }
+ }
+
+ private void addIndexField(String indexName,SDField field) {
+ List<SDField> fields=indexToFields.get(indexName);
+ if (fields==null) {
+ fields=new java.util.ArrayList<>();
+ indexToFields.put(indexName,fields);
+ }
+ fields.add(field);
+ }
+
+ private void resolveAllConflicts(Search search) {
+ for (Map.Entry<String, List<SDField>> entry : indexToFields.entrySet()) {
+ String indexName = entry.getKey();
+ List<SDField> fields = entry.getValue();
+ if (fields.size() == 1) continue; // It takes two to make a conflict
+ resolveConflicts(indexName, fields, search);
+ }
+ }
+
+ /**
+ * Resolves all conflicts for one index
+ *
+ * @param indexName the name of the index in question
+ * @param fields all the fields indexed to this index
+ * @param search the search definition having this
+ */
+ private void resolveConflicts(String indexName,List<SDField> fields,Search search) {
+ new StemmingResolver(indexName, fields, search, deployLogger).resolve();
+ new IndexCommandResolver(indexName, fields, search, deployLogger).resolve();
+ new RankTypeResolver(indexName, fields, search, deployLogger).resolve();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/NGramMatch.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/NGramMatch.java
new file mode 100644
index 00000000000..8e5122cc158
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/NGramMatch.java
@@ -0,0 +1,76 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * The implementation of "gram" matching - splitting the incoming text and the queries into
+ * n-grams for matching. This will also validate the gram settings.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class NGramMatch extends Processor {
+
+ public static final int DEFAULT_GRAM_SIZE = 2;
+
+ public NGramMatch(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getMatching().getType().equals(Matching.Type.GRAM))
+ implementGramMatch(search, field);
+ else if (field.getMatching().getGramSize()>=0)
+ throw new IllegalArgumentException("gram-size can only be set when the matching mode is 'gram'");
+ }
+ }
+
+ private void implementGramMatch(Search search, SDField field) {
+ if (field.doesAttributing())
+ throw new IllegalArgumentException("gram matching is not supported with attributes, use 'index' not 'attribute' in indexing");
+
+ int n = field.getMatching().getGramSize();
+ if (n<0)
+ n=DEFAULT_GRAM_SIZE; // not set - use default gram size
+ if (n==0)
+ throw new IllegalArgumentException("Illegal gram size in " + field + ": Must be at least 1");
+ field.getNormalizing().inferCodepoint();
+ field.setStemming(Stemming.NONE); // not compatible with stemming and normalizing
+ field.addQueryCommand("ngram " + n);
+ field.setIndexingScript((ScriptExpression)new MyProvider(search, n).convert(field.getIndexingScript()));
+ }
+
+ private static class MyProvider extends TypedTransformProvider {
+
+ final int ngram;
+
+ MyProvider(Search search, int ngram) {
+ super(NGramExpression.class, search);
+ this.ngram = ngram;
+ }
+
+ @Override
+ protected boolean requiresTransform(Expression exp, DataType fieldType) {
+ return exp instanceof OutputExpression;
+ }
+
+ @Override
+ protected Expression newTransform(DataType fieldType) {
+ Expression exp = new NGramExpression(null, ngram);
+ if (fieldType instanceof CollectionDataType) {
+ exp = new ForEachExpression(exp);
+ }
+ return exp;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/OptimizeIlscript.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/OptimizeIlscript.java
new file mode 100644
index 00000000000..078e9400ec5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/OptimizeIlscript.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.indexinglanguage.ExpressionOptimizer;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Run ExpressionOptimizer on all scripts, to get rid of expressions that have no effect.
+ */
+public class OptimizeIlscript extends Processor {
+ public OptimizeIlscript(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ ScriptExpression script = field.getIndexingScript();
+ if (script == null) {
+ continue;
+ }
+ field.setIndexingScript((ScriptExpression)new ExpressionOptimizer().convert(script));
+ if (!field.getIndexingScript().toString().equals(script.toString())) {
+ warn(search, field, "Rewrote ilscript from:\n" + script.toString() + "\nto\n" + field.getIndexingScript().toString());
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/PredicateProcessor.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/PredicateProcessor.java
new file mode 100644
index 00000000000..a156a98a584
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/PredicateProcessor.java
@@ -0,0 +1,138 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.document.datatypes.IntegerFieldValue;
+import com.yahoo.document.datatypes.LongFieldValue;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.BooleanIndexDefinition;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Validates the predicate fields.
+ *
+ * @author <a href="mailto:lesters@yahoo-inc.com">Lester Solbakken</a>
+ */
+public class PredicateProcessor extends Processor {
+
+ public PredicateProcessor(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getDataType() == DataType.PREDICATE) {
+ if (field.doesIndexing()) {
+ fail(search, field, "Use 'attribute' instead of 'index'. This will require a refeed if you have upgraded.");
+ }
+ if (field.doesAttributing()) {
+ Attribute attribute = field.getAttributes().get(field.getName());
+ for (Index index : field.getIndices().values()) {
+ BooleanIndexDefinition booleanDefinition = index.getBooleanIndexDefiniton();
+ if (booleanDefinition == null || !booleanDefinition.hasArity()) {
+ fail(search, field, "Missing arity value in predicate field.");
+ }
+ if (booleanDefinition.getArity() < 2) {
+ fail(search, field, "Invalid arity value in predicate field, must be greater than 1.");
+ }
+ double threshold = booleanDefinition.getDensePostingListThreshold();
+ if (threshold <= 0 || threshold > 1) {
+ fail(search, field, "Invalid dense-posting-list-threshold value in predicate field. " +
+ "Value must be in range (0..1].");
+ }
+
+ attribute.setArity(booleanDefinition.getArity());
+ attribute.setLowerBound(booleanDefinition.getLowerBound());
+ attribute.setUpperBound(booleanDefinition.getUpperBound());
+
+ attribute.setDensePostingListThreshold(threshold);
+ addPredicateOptimizationIlScript(field, booleanDefinition);
+ }
+ DocumentSummary summary = search.getSummary("attributeprefetch");
+ if (summary != null) {
+ summary.remove(attribute.getName());
+ }
+ for (SummaryField summaryField : search.getSummaryFields(field).values()) {
+ summaryField.setTransform(SummaryTransform.NONE);
+ }
+ }
+ } else if (field.getDataType().getPrimitiveType() == DataType.PREDICATE) {
+ fail(search, field, "Collections of predicates are not allowed.");
+ } else if (field.getDataType() == DataType.RAW && field.doesIndexing()) {
+ fail(search, field, "Indexing of RAW fields are not supported. If you are using RAW fields for boolean search, use predicate data type instead.");
+ } else {
+ // if field is not a predicate, disallow predicate-related index parameters
+ for (Index index : field.getIndices().values()) {
+ if (index.getBooleanIndexDefiniton() != null) {
+ BooleanIndexDefinition def = index.getBooleanIndexDefiniton();
+ if (def.hasArity()) {
+ fail(search, field, "Arity parameter is used only for predicate type fields.");
+ } else if (def.hasLowerBound() || def.hasUpperBound()) {
+ fail(search, field, "Parameters lower-bound and upper-bound are used only for predicate type fields.");
+ } else if (def.hasDensePostingListThreshold()) {
+ fail(search, field, "Parameter dense-posting-list-threshold is used only for predicate type fields.");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void addPredicateOptimizationIlScript(SDField field, BooleanIndexDefinition booleanIndexDefiniton) {
+ Expression script = field.getIndexingScript();
+ if (script == null) {
+ return;
+ }
+ script = new StatementExpression(makeSetPredicateVariablesScript(booleanIndexDefiniton), script);
+
+ ExpressionConverter converter = new PredicateOutputTransformer(search);
+ field.setIndexingScript(new ScriptExpression((StatementExpression)converter.convert(script)));
+ }
+
+ private Expression makeSetPredicateVariablesScript(BooleanIndexDefinition options) {
+ List<Expression> expressions = new ArrayList<>();
+ expressions.add(new SetValueExpression(new IntegerFieldValue(options.getArity())));
+ expressions.add(new SetVarExpression("arity"));
+ if (options.hasLowerBound()) {
+ expressions.add(new SetValueExpression(new LongFieldValue(options.getLowerBound())));
+ expressions.add(new SetVarExpression("lower_bound"));
+ }
+ if (options.hasUpperBound()) {
+ expressions.add(new SetValueExpression(new LongFieldValue(options.getUpperBound())));
+ expressions.add(new SetVarExpression("upper_bound"));
+ }
+ return new StatementExpression(expressions);
+ }
+
+ private static class PredicateOutputTransformer extends TypedTransformProvider {
+
+ PredicateOutputTransformer(Search search) {
+ super(OptimizePredicateExpression.class, search);
+ }
+
+ @Override
+ protected boolean requiresTransform(Expression exp, DataType fieldType) {
+ return exp instanceof OutputExpression && fieldType == DataType.PREDICATE;
+ }
+
+ @Override
+ protected Expression newTransform(DataType fieldType) {
+ return new OptimizePredicateExpression();
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java
new file mode 100644
index 00000000000..74f359602bd
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processing.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.processing.multifieldresolver.RankProfileTypeSettingsProcessor;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Executor of processors. This defines the right order of processor execution.
+ *
+ * @author bratseth
+ */
+public class Processing {
+
+ /**
+ * Runs all search processors on the given {@link Search} object. These will modify the search object, <b>possibly
+ * exchanging it with another</b>, as well as its document types.
+ *
+ * @param search The search to process.
+ * @param deployLogger The log to log messages and warnings for application deployment to
+ * @param rankProfileRegistry a {@link com.yahoo.searchdefinition.RankProfileRegistry}
+ * @param queryProfiles The query profiles contained in the application this search is part of.
+ */
+ public static void process(Search search,
+ DeployLogger deployLogger,
+ RankProfileRegistry rankProfileRegistry,
+ QueryProfiles queryProfiles) {
+ search.process();
+ new UrlFieldValidator(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new BuiltInFieldSets(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SearchMustHaveDocument(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new ReservedDocumentNames(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new IndexFieldNames(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new IntegerIndex2Attribute(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new MakeAliases(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SetLanguage(search, deployLogger, rankProfileRegistry, queryProfiles).process(); // Needs to come before UriHack, see ticket 6405470
+ new UriHack(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new LiteralBoost(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new IndexTo2FieldSet(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new TagType(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new IndexingInputs(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new OptimizeIlscript(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new ValidateFieldWithIndexSettingsCreatesIndex(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new AttributesImplicitWord(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new CreatePositionZCurve(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new WordMatch(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new DeprecateAttributePrefetch(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new ImplicitSummaries(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new ImplicitSummaryFields(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SummaryConsistency(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SummaryNamesFieldCollisions(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SummaryFieldsMustHaveValidSource(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new MakeDefaultSummaryTheSuperSet(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new Bolding(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new AttributeProperties(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SetRankTypeEmptyOnFilters(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new IndexSettingsNonFieldNames(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SummaryDynamicStructsArrays(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new StringSettingsOnNonStringFields(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new IndexingOutputs(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new ExactMatch(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new NGramMatch(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new TextMatch(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new MultifieldIndexHarmonizer(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new FilterFieldNames(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new MatchConsistency(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new ValidateFieldTypes(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new DisallowComplexMapAndWsetKeyTypes(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new SortingSettings(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new FieldSetValidity(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new AddExtraFieldsToDocument(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new PredicateProcessor(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new MatchPhaseSettingsValidator(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new DiversitySettingsValidator(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new TensorFieldProcessor(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new RankProfileTypeSettingsProcessor(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+
+ // These two should be last.
+ new IndexingValidation(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ new IndexingValues(search, deployLogger, rankProfileRegistry, queryProfiles).process();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processor.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processor.java
new file mode 100644
index 00000000000..a3e0484b2fd
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/Processor.java
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * Abstract superclass of all search definition processors.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public abstract class Processor {
+
+ protected final Search search;
+ protected DeployLogger deployLogger;
+ protected final RankProfileRegistry rankProfileRegistry;
+ protected final QueryProfiles queryProfiles;
+
+ /**
+ * Base constructor
+ * @param search the search to process
+ * @param deployLogger Logger du use when logging deploy output.
+ * @param rankProfileRegistry Registry with all rank profiles, used for lookup and insertion.
+ * @param queryProfiles The query profiles contained in the application this search is part of.
+ */
+ public Processor(Search search, DeployLogger deployLogger,
+ RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ this.search = search;
+ this.deployLogger = deployLogger;
+ this.rankProfileRegistry = rankProfileRegistry;
+ this.queryProfiles = queryProfiles;
+ }
+
+ /**
+ * Processes the input search definition by <b>modifying</b> the input search and its documents, and returns the
+ * input search definition.
+ */
+ public abstract void process();
+
+ /**
+ * Convenience method for adding a no-strings-attached implementation field for a regular field
+ *
+ * @param search the search definition in question
+ * @param field the field to add an implementation field for
+ * @param suffix the suffix of the added implementation field (without the underscore)
+ * @param indexing the indexing statement of the field
+ * @param queryCommand the query command of the original field, or null if none
+ * @return the implementation field which is added to the search
+ */
+ protected SDField addField(Search search, SDField field, String suffix, String indexing, String queryCommand) {
+ SDField implementationField = search.getField(field.getName() + "_" + suffix);
+ if (implementationField != null) {
+ deployLogger.log(Level.WARNING, "Implementation field " + implementationField + " added twice");
+ } else {
+ implementationField = new SDField(search.getDocument(), field.getName() + "_" + suffix, DataType.STRING);
+ }
+ implementationField.setRankType(RankType.EMPTY);
+ implementationField.setStemming(Stemming.NONE);
+ implementationField.getNormalizing().inferCodepoint();
+ implementationField.parseIndexingScript(indexing);
+ for (Iterator i = field.getFieldNameAsIterator(); i.hasNext();) {
+ String indexName = (String)i.next();
+ String implementationIndexName = indexName + "_" + suffix;
+ Index implementationIndex = new Index(implementationIndexName);
+ search.addIndex(implementationIndex);
+ }
+ if (queryCommand != null) {
+ field.addQueryCommand(queryCommand);
+ }
+ search.addExtraField(implementationField);
+ search.fieldSets().addBuiltInFieldSetItem(BuiltInFieldSets.INTERNAL_FIELDSET_NAME, implementationField.getName());
+ return implementationField;
+ }
+
+ /**
+ * Returns an iterator of all the rank settings with given type in all the rank profiles in this search
+ * definition.
+ */
+ protected Iterator<RankProfile.RankSetting> matchingRankSettingsIterator(
+ Search search, RankProfile.RankSetting.Type type)
+ {
+ List<RankProfile.RankSetting> someRankSettings = new java.util.ArrayList<>();
+
+ for (RankProfile profile : rankProfileRegistry.localRankProfiles(search)) {
+ for (Iterator j = profile.declaredRankSettingIterator(); j.hasNext(); ) {
+ RankProfile.RankSetting setting = (RankProfile.RankSetting)j.next();
+ if (setting.getType().equals(type)) {
+ someRankSettings.add(setting);
+ }
+ }
+ }
+ return someRankSettings.iterator();
+ }
+
+ protected String formatError(String searchName, String fieldName, String msg) {
+ return "For search '" + searchName + "', field '" + fieldName + "': " + msg;
+ }
+
+ protected RuntimeException newProcessException(String searchName, String fieldName, String msg) {
+ return new IllegalArgumentException(formatError(searchName, fieldName, msg));
+ }
+
+ protected RuntimeException newProcessException(Search search, Field field, String msg) {
+ return newProcessException(search.getName(), field.getName(), msg);
+ }
+
+ public void fail(Search search, Field field, String msg) {
+ throw newProcessException(search, field, msg);
+ }
+
+ protected void warn(String searchName, String fieldName, String msg) {
+ String fullMsg = formatError(searchName, fieldName, msg);
+ deployLogger.log(Level.WARNING, fullMsg);
+ }
+
+ protected void warn(Search search, Field field, String msg) {
+ warn(search.getName(), field.getName(), msg);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/ReservedDocumentNames.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ReservedDocumentNames.java
new file mode 100644
index 00000000000..74bf2d35bac
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ReservedDocumentNames.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ReservedDocumentNames extends Processor {
+
+ private static final Set<String> RESERVED_NAMES = new HashSet<>();
+ static {
+ for (SDDocumentType dataType : SDDocumentType.VESPA_DOCUMENT.getTypes()) {
+ RESERVED_NAMES.add(dataType.getName());
+ }
+ }
+
+ public ReservedDocumentNames(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ String docName = search.getDocument().getName();
+ if (RESERVED_NAMES.contains(docName)) {
+ throw new IllegalArgumentException("For search '" + search.getName() + "': Document name '" + docName +
+ "' is reserved.");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SearchMustHaveDocument.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SearchMustHaveDocument.java
new file mode 100644
index 00000000000..5b45aecc257
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SearchMustHaveDocument.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * A search must have a document definition of the same name inside of it, otherwise crashes may occur as late as
+ * during feeding
+ * @author vegardh
+ *
+ */
+public class SearchMustHaveDocument extends Processor {
+
+ public SearchMustHaveDocument(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ if (search.getDocument()==null) {
+ throw new IllegalArgumentException("For search '" + search.getName() + "': A search specification must have an equally named document inside of it.");
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SetLanguage.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SetLanguage.java
new file mode 100644
index 00000000000..764bb1602a7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SetLanguage.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.indexinglanguage.expressions.SetLanguageExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Check that no text field appears before a field that sets language.
+ *
+ * @author <a href="mailto:gunnarga@yahoo-inc.com">Gunnar Gauslaa Bergem</a>
+ */
+public class SetLanguage extends Processor {
+
+ public SetLanguage(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ List<String> textFieldsWithoutLanguage = new ArrayList<>();
+
+ for (SDField field : search.allFieldsList()) {
+ if (fieldMustComeAfterLanguageSettingField(field)) {
+ textFieldsWithoutLanguage.add(field.getName());
+ }
+ if (field.containsExpression(SetLanguageExpression.class) && !textFieldsWithoutLanguage.isEmpty()) {
+ StringBuffer fieldString = new StringBuffer();
+ for (String fieldName : textFieldsWithoutLanguage) {
+ fieldString.append(fieldName).append(" ");
+ }
+ warn(search, field, "Field '" + field.getName() + "' sets the language for this document, " +
+ "and should be defined as the first field in the searchdefinition. If you have both header and body fields, this field "+
+ "should be header, if you require it to affect subsequent header fields and/or any body fields. " +
+ "Preceding text fields that will not have their language set: " +
+ fieldString.toString() +
+ " (This warning is omitted for any subsequent fields that also do set_language.)");
+ return;
+ }
+ }
+ }
+
+ private boolean fieldMustComeAfterLanguageSettingField(SDField field) {
+ return (!field.containsExpression(SetLanguageExpression.class) &&
+ (field.getDataType() == DataType.STRING));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SetRankTypeEmptyOnFilters.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SetRankTypeEmptyOnFilters.java
new file mode 100644
index 00000000000..bc676b64a71
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SetRankTypeEmptyOnFilters.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * All rank: filter fields should have rank type empty.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class SetRankTypeEmptyOnFilters extends Processor {
+
+ public SetRankTypeEmptyOnFilters(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getRanking().isFilter()) {
+ field.setRankType(RankType.EMPTY);
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SortingSettings.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SortingSettings.java
new file mode 100644
index 00000000000..6e77f48b4a7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SortingSettings.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Sorting;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Validate conflicting settings for sorting
+ * @author vegardh
+ *
+ */
+public class SortingSettings extends Processor {
+
+ public SortingSettings(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ for (Attribute attribute : field.getAttributes().values()) {
+ Sorting sorting = attribute.getSorting();
+ if (sorting.getFunction()!=Sorting.Function.UCA) {
+ if (sorting.getStrength()!=null && sorting.getStrength()!=Sorting.Strength.PRIMARY) {
+ warn(search, field, "Sort strength only works for sort function 'uca'.");
+ }
+ if (sorting.getLocale()!=null && !"".equals(sorting.getLocale())) {
+ warn(search, field, "Sort locale only works for sort function 'uca'.");
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/StringSettingsOnNonStringFields.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/StringSettingsOnNonStringFields.java
new file mode 100644
index 00000000000..48c9af5556a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/StringSettingsOnNonStringFields.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.NumericDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+public class StringSettingsOnNonStringFields extends Processor {
+
+ public StringSettingsOnNonStringFields(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (!doCheck(field)) continue;
+ if (field.getMatching().isTypeUserSet()) {
+ warn(search, field, "Matching type "+field.getMatching().getType()+" is only allowed for string fields.");
+ }
+ if (field.getRanking().isLiteral()) {
+ warn(search, field, "Rank type literal only applies to string fields");
+ }
+ }
+ }
+
+ private boolean doCheck(SDField field) {
+ if (field.getDataType() instanceof NumericDataType) return true;
+ if (field.getDataType() instanceof CollectionDataType) {
+ if (((CollectionDataType)field.getDataType()).getNestedType() instanceof NumericDataType) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryConsistency.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryConsistency.java
new file mode 100644
index 00000000000..20eeb5f0810
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryConsistency.java
@@ -0,0 +1,102 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.document.WeightedSetDataType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Ensure that summary field transforms for fields having the same name
+ * are consistent across summary classes
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class SummaryConsistency extends Processor {
+
+ public SummaryConsistency(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (DocumentSummary summary : search.getSummaries().values()) {
+ if (summary.getName().equals("default")) continue;
+ for (SummaryField summaryField : summary.getSummaryFields() ) {
+ assertConsistency(summaryField,search);
+ makeAttributeTransformIfAppropriate(summaryField,search);
+ }
+ }
+ }
+
+ /** If the source is an attribute, make this use the attribute transform */
+ private void makeAttributeTransformIfAppropriate(SummaryField summaryField,Search search) {
+ if (summaryField.getTransform()!=SummaryTransform.NONE) return;
+ Attribute attribute=search.getAttribute(summaryField.getSingleSource());
+ if (attribute==null) return;
+ summaryField.setTransform(SummaryTransform.ATTRIBUTE);
+ }
+
+ private void assertConsistency(SummaryField summaryField,Search search) {
+ SummaryField existingDefault=search.getSummary("default").getSummaryField(summaryField.getName()); // Compare to default
+ if (existingDefault!=null) {
+ assertConsistentTypes(existingDefault,summaryField);
+ makeConsistentWithDefaultOrThrow(existingDefault,summaryField);
+ }
+ else {
+ // If no default, compare to whichever definition of the field
+ SummaryField existing=search.getExplicitSummaryField(summaryField.getName());
+ if (existing==null) return;
+ assertConsistentTypes(existing,summaryField);
+ makeConsistentOrThrow(existing,summaryField,search);
+ }
+ }
+
+ private void assertConsistentTypes(SummaryField field1,SummaryField field2) {
+ if (field1.getDataType() instanceof WeightedSetDataType && field2.getDataType() instanceof WeightedSetDataType &&
+ ((WeightedSetDataType)field1.getDataType()).getNestedType().equals(((WeightedSetDataType)field2.getDataType()).getNestedType()))
+ return; // Disregard create-if-nonexistent and create-if-zero distinction
+ if ( ! field1.getDataType().equals(field2.getDataType()))
+ throw new IllegalArgumentException(field1.toLocateString() + " is inconsistent with " + field2.toLocateString() + ": All declarations of the same summary field must have the same type");
+ }
+
+ private void makeConsistentOrThrow(SummaryField field1, SummaryField field2,Search search) {
+ if (field2.getTransform()==SummaryTransform.ATTRIBUTE && field1.getTransform()==SummaryTransform.NONE) {
+ Attribute attribute=search.getAttribute(field1.getName());
+ if (attribute != null) {
+ field1.setTransform(SummaryTransform.ATTRIBUTE);
+ }
+ }
+
+ if (field2.getTransform().equals(SummaryTransform.NONE)) {
+ field2.setTransform(field1.getTransform());
+ }
+ else { // New field sets an explicit transform - must be the same
+ assertEqualTransform(field1,field2);
+ }
+ }
+ private void makeConsistentWithDefaultOrThrow(SummaryField defaultField,SummaryField newField) {
+ if (newField.getTransform().equals(SummaryTransform.NONE)) {
+ newField.setTransform(defaultField.getTransform());
+ }
+ else { // New field sets an explicit transform - must be the same
+ assertEqualTransform(defaultField,newField);
+ }
+ }
+
+
+ private void assertEqualTransform(SummaryField field1,SummaryField field2) {
+ if (!field2.getTransform().equals(field1.getTransform())) {
+ throw new IllegalArgumentException("Conflicting summary transforms. " + field2 +" is already defined as " +
+ field1 + ". A field with the same name " +
+ "can not have different transforms in different summary classes");
+ }
+ }
+
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryDynamicStructsArrays.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryDynamicStructsArrays.java
new file mode 100644
index 00000000000..f97d5345cab
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryDynamicStructsArrays.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Fail if:
+ * An SD field explicitly says summary:dynamic , but the field is wset, array or struct.
+ * If there is an explicitly defined summary class, saying dynamic in one of its summary
+ * fields is always legal.
+ * @author vegardh
+ *
+ */
+public class SummaryDynamicStructsArrays extends Processor {
+
+ public SummaryDynamicStructsArrays(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ DataType type = field.getDataType();
+ if (type instanceof ArrayDataType || type instanceof WeightedSetDataType
+ || type instanceof StructDataType) {
+ for (SummaryField sField : field.getSummaryFields()) {
+ if (sField.getTransform().equals(SummaryTransform.DYNAMICTEASER)) {
+ throw new IllegalArgumentException("For field '"+field.getName()+"': dynamic summary is illegal " +
+ "for fields of type struct, array or weighted set. Use an explicit summary class with explicit summary fields sourcing from" +
+ " the array/struct/weighted set.");
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSource.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSource.java
new file mode 100644
index 00000000000..0d76ada0d52
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSource.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Verifies that the source fields actually refers to a valid field.
+ *
+ * @author balder
+ *
+ */
+public class SummaryFieldsMustHaveValidSource extends Processor {
+ SummaryFieldsMustHaveValidSource(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+ @Override
+ public void process() {
+ for (DocumentSummary summary : search.getSummaries().values()) {
+ for (SummaryField summaryField : summary.getSummaryFields()) {
+ if (summaryField.getSources().isEmpty()) {
+ if ((summaryField.getTransform() != SummaryTransform.RANKFEATURES) &&
+ (summaryField.getTransform() != SummaryTransform.SUMMARYFEATURES))
+ {
+ verifySource(summaryField.getName(), summaryField, summary);
+ }
+ } else if (summaryField.getSourceCount() == 1) {
+ verifySource(summaryField.getSingleSource(), summaryField, summary);
+ } else {
+ for (SummaryField.Source source : summaryField.getSources()) {
+ if ( ! source.getName().equals(summaryField.getName()) ) {
+ verifySource(source.getName(), summaryField, summary);
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+ private boolean isValid(String source, SummaryField summaryField, DocumentSummary summary) {
+ return isDocumentField(source) ||
+ (isNotInThisSummaryClass(summary, source) && isSummaryField(source)) ||
+ (isInThisSummaryClass(summary, source) && !source.equals(summaryField.getName())) ||
+ ("documentid".equals(source));
+ }
+
+ private void verifySource(String source, SummaryField summaryField, DocumentSummary summary) {
+ if ( ! isValid(source, summaryField, summary) ) {
+ throw new IllegalArgumentException("For search '" + search.getName() + "', summary class '" + summary.getName() + "'," +
+ " summary field '" + summaryField.getName() + "': there is no valid source '" + source + "'.");
+ }
+ }
+
+ private boolean isNotInThisSummaryClass(DocumentSummary summary, String name) {
+ return summary.getSummaryField(name) == null;
+ }
+ private boolean isInThisSummaryClass(DocumentSummary summary, String name) {
+ return summary.getSummaryField(name) != null;
+ }
+ private boolean isDocumentField(String name) {
+ return search.getField(name) != null;
+ }
+
+ private boolean isSummaryField(String name) {
+ return search.getSummaryField(name) != null;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryNamesFieldCollisions.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryNamesFieldCollisions.java
new file mode 100644
index 00000000000..8d99611d697
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/SummaryNamesFieldCollisions.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryField.Source;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Verifies that equally named summary fields in different summary classes don't use different fields for source.
+ * The summarymap config doesn't model this.
+ *
+ * @author vegardh
+ *
+ */
+public class SummaryNamesFieldCollisions extends Processor {
+
+ public SummaryNamesFieldCollisions(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ Map<String, Pair<String, String>> fieldToClassAndSource = new HashMap<>();
+ for (DocumentSummary summary : search.getSummaries().values()) {
+ if ("default".equals(summary.getName())) continue;
+ for (SummaryField summaryField : summary.getSummaryFields() ) {
+ if (summaryField.isImplicit()) continue;
+ Pair<String, String> prevClassAndSource = fieldToClassAndSource.get(summaryField.getName());
+ for (Source source : summaryField.getSources()) {
+ if (prevClassAndSource!=null) {
+ String prevClass = prevClassAndSource.getFirst();
+ String prevSource = prevClassAndSource.getSecond();
+ if (!prevClass.equals(summary.getName())) {
+ if (!prevSource.equals(source.getName())) {
+ throw new IllegalArgumentException("For search '"+search.getName()+"', summary class '"+summary.getName()+"'," +
+ " summary field '"+summaryField.getName()+"':" +
+ " Can not use source '"+source.getName()+"' for this summary field, an equally named field in summary class '" +
+ prevClass + "' uses a different source: '"+prevSource+"'.");
+ }
+ }
+ } else {
+ fieldToClassAndSource.put(summaryField.getName(), new Pair<>(summary.getName(), source.getName()));
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/TagType.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TagType.java
new file mode 100644
index 00000000000..3e211f4f632
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TagType.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * The implementation of the tag datatype
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class TagType extends Processor {
+
+ public TagType(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getDataType() instanceof WeightedSetDataType && ((WeightedSetDataType)field.getDataType()).isTag()) {
+ implementTagType(field);
+ }
+ }
+ }
+
+ private void implementTagType(SDField field) {
+ field.setDataType(DataType.getWeightedSet(DataType.STRING,true,true));
+ // Don't set matching and ranking if this field is not attribute nor index
+ if (!field.doesIndexing() && !field.doesAttributing()) return;
+ Matching m = field.getMatching();
+ if (!m.isTypeUserSet()) {
+ m.setType(Matching.Type.WORD);
+ }
+ if (field.getRankType()==null || field.getRankType()== RankType.DEFAULT)
+ field.setRankType((RankType.TAGS));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/TensorFieldProcessor.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TensorFieldProcessor.java
new file mode 100644
index 00000000000..eb4efbe377f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TensorFieldProcessor.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Class that processes and validates tensor fields.
+ *
+ * @author <a href="geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class TensorFieldProcessor extends Processor {
+
+ public TensorFieldProcessor(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getDataType() == DataType.TENSOR) {
+ warnUseOfTensorFieldAsAttribute(field);
+ validateIndexingScripsForTensorField(field);
+ validateAttributeSettingForTensorField(field);
+ } else {
+ validateDataTypeForField(field);
+ }
+ }
+ }
+
+ private void warnUseOfTensorFieldAsAttribute(SDField field) {
+ if (field.doesAttributing()) {
+ // TODO (geirst): Remove when no longer beta
+ warn(search, field, "An attribute of type 'tensor' is currently beta, and re-feeding data between Vespa versions might be required.");
+ }
+ }
+
+ private void validateIndexingScripsForTensorField(SDField field) {
+ if (field.doesIndexing()) {
+ fail(search, field, "A field of type 'tensor' cannot be specified as an 'index' field.");
+ }
+ }
+
+ private void validateAttributeSettingForTensorField(SDField field) {
+ if (field.doesAttributing()) {
+ Attribute attribute = field.getAttributes().get(field.getName());
+ if (attribute != null && attribute.isFastSearch()) {
+ fail(search, field, "An attribute of type 'tensor' cannot be 'fast-search'.");
+ }
+ }
+ }
+
+ private void validateDataTypeForField(SDField field) {
+ if (field.getDataType().getPrimitiveType() == DataType.TENSOR) {
+ fail(search, field, "A field with collection type of tensor is not supported. Use simple type 'tensor' instead.");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/TextMatch.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TextMatch.java
new file mode 100644
index 00000000000..21723639ece
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TextMatch.java
@@ -0,0 +1,122 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.ExpressionVisitor;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.expressions.ForEachExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.IndexExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.OutputExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.SummaryExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.TokenizeExpression;
+import com.yahoo.vespa.indexinglanguage.linguistics.AnnotatorConfig;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class TextMatch extends Processor {
+
+ public TextMatch(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.getMatching().getType() != Matching.Type.TEXT) {
+ continue;
+ }
+ ScriptExpression script = field.getIndexingScript();
+ if (script == null) {
+ continue;
+ }
+ DataType fieldType = field.getDataType();
+ if (fieldType instanceof CollectionDataType) {
+ fieldType = ((CollectionDataType)fieldType).getNestedType();
+ }
+ if (fieldType != DataType.STRING) {
+ continue;
+ }
+ Set<String> dynamicSummary = new TreeSet<>();
+ Set<String> staticSummary = new TreeSet<>();
+ new IndexingOutputs(search, deployLogger, rankProfileRegistry, queryProfiles).findSummaryTo(search, field, dynamicSummary, staticSummary);
+ MyVisitor visitor = new MyVisitor(dynamicSummary);
+ visitor.visit(script);
+ if (!visitor.requiresTokenize) {
+ continue;
+ }
+ ExpressionConverter converter = new MyStringTokenizer(search, findAnnotatorConfig(search, field));
+ field.setIndexingScript((ScriptExpression)converter.convert(script));
+ }
+ }
+
+ private static AnnotatorConfig findAnnotatorConfig(Search search, SDField field) {
+ AnnotatorConfig ret = new AnnotatorConfig();
+ Stemming activeStemming = field.getStemming();
+ if (activeStemming == null) {
+ activeStemming = search.getStemming();
+ }
+ ret.setStemMode(activeStemming.toStemMode());
+ ret.setRemoveAccents(field.getNormalizing().doRemoveAccents());
+ return ret;
+ }
+
+ private static class MyVisitor extends ExpressionVisitor {
+
+ final Set<String> dynamicSummaryFields;
+ boolean requiresTokenize = false;
+
+ MyVisitor(Set<String> dynamicSummaryFields) {
+ this.dynamicSummaryFields = dynamicSummaryFields;
+ }
+
+ @Override
+ protected void doVisit(Expression exp) {
+ if (exp instanceof IndexExpression) {
+ requiresTokenize = true;
+ }
+ if (exp instanceof SummaryExpression &&
+ dynamicSummaryFields.contains(((SummaryExpression)exp).getFieldName()))
+ {
+ requiresTokenize = true;
+ }
+ }
+ }
+
+ private static class MyStringTokenizer extends TypedTransformProvider {
+
+ final AnnotatorConfig annotatorCfg;
+
+ MyStringTokenizer(Search search, AnnotatorConfig annotatorCfg) {
+ super(TokenizeExpression.class, search);
+ this.annotatorCfg = annotatorCfg;
+ }
+
+ @Override
+ protected boolean requiresTransform(Expression exp, DataType fieldType) {
+ return exp instanceof OutputExpression;
+ }
+
+ @Override
+ protected Expression newTransform(DataType fieldType) {
+ Expression exp = new TokenizeExpression(null, annotatorCfg);
+ if (fieldType instanceof CollectionDataType) {
+ exp = new ForEachExpression(exp);
+ }
+ return exp;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/TypedTransformProvider.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TypedTransformProvider.java
new file mode 100644
index 00000000000..0fa9cbfa05f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/TypedTransformProvider.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.vespa.indexinglanguage.ValueTransformProvider;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class TypedTransformProvider extends ValueTransformProvider {
+
+ private final Search search;
+ private DataType fieldType;
+
+ TypedTransformProvider(Class<? extends Expression> transformClass, Search search) {
+ super(transformClass);
+ this.search = search;
+ }
+
+ @Override
+ protected final boolean requiresTransform(Expression exp) {
+ if (exp instanceof OutputExpression) {
+ String fieldName = ((OutputExpression)exp).getFieldName();
+ if (exp instanceof AttributeExpression) {
+ Attribute attribute = search.getAttribute(fieldName);
+ if (attribute == null) {
+ throw new IllegalArgumentException("Attribute '" + fieldName + "' not found.");
+ }
+ fieldType = attribute.getDataType();
+ } else if (exp instanceof IndexExpression) {
+ Field field = search.getField(fieldName);
+ if (field == null) {
+ throw new IllegalArgumentException("Index field '" + fieldName + "' not found.");
+ }
+ fieldType = field.getDataType();
+ } else if (exp instanceof SummaryExpression) {
+ Field field = search.getSummaryField(fieldName);
+ if (field == null) {
+ throw new IllegalArgumentException("Summary field '" + fieldName + "' not found.");
+ }
+ fieldType = field.getDataType();
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+ return requiresTransform(exp, fieldType);
+ }
+
+ @Override
+ protected final Expression newTransform() {
+ return newTransform(fieldType);
+ }
+
+ protected abstract boolean requiresTransform(Expression exp, DataType fieldType);
+
+ protected abstract Expression newTransform(DataType fieldType);
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/UriHack.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/UriHack.java
new file mode 100644
index 00000000000..8f9b88b0268
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/UriHack.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author balder
+ */
+public class UriHack extends Processor {
+ private static final List<String> URL_SUFFIX =
+ Arrays.asList("scheme", "host", "port", "path", "query", "fragment", "hostname");
+ public UriHack(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (field.doesIndexing()) {
+ DataType fieldType = field.getDataType();
+ if (fieldType instanceof CollectionDataType) {
+ fieldType = ((CollectionDataType)fieldType).getNestedType();
+ }
+ if (fieldType == DataType.URI) {
+ processField(search, field);
+ }
+ }
+ }
+ }
+
+ private void processField(Search search, SDField uriField) {
+ String uriName = uriField.getName();
+ uriField.setStemming(Stemming.NONE);
+ DataType generatedType = DataType.STRING;
+ if (uriField.getDataType() instanceof ArrayDataType) {
+ generatedType = new ArrayDataType(DataType.STRING);
+ } else if (uriField.getDataType() instanceof WeightedSetDataType) {
+ WeightedSetDataType wdt = (WeightedSetDataType) uriField.getDataType();
+ generatedType = new WeightedSetDataType(DataType.STRING, wdt.createIfNonExistent(), wdt.removeIfZero());
+ }
+ for (String suffix : URL_SUFFIX) {
+ String partName = uriName + "." + suffix;
+ // I wonder if this is explicit in qrs or implicit in backend?
+ // search.addFieldSetItem(uriName, partName);
+ SDField partField = new SDField(partName, generatedType, true);
+ partField.setIndexStructureField(uriField.doesIndexing());
+ partField.setRankType(uriField.getRankType());
+ partField.setStemming(Stemming.NONE);
+ partField.getNormalizing().inferLowercase();
+ if (uriField.getIndex(suffix) != null) {
+ partField.addIndex(uriField.getIndex(suffix));
+ }
+ search.addExtraField(partField);
+ search.fieldSets().addBuiltInFieldSetItem(BuiltInFieldSets.INTERNAL_FIELDSET_NAME, partField.getName());
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/UrlFieldValidator.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/UrlFieldValidator.java
new file mode 100644
index 00000000000..b01a82b0131
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/UrlFieldValidator.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * @author bratseth
+ */
+public class UrlFieldValidator extends Processor {
+
+ public UrlFieldValidator(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if ( ! field.getDataType().equals(DataType.URI)) continue;
+
+ if (field.doesAttributing())
+ throw new IllegalArgumentException("Error in " + field + " in " + search + ": " +
+ "uri type fields cannot be attributes");
+ }
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldTypes.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldTypes.java
new file mode 100644
index 00000000000..38c7338be3c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldTypes.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This Processor checks to make sure all fields with the same name have the same {@link DataType}. This check
+ * explicitly disregards whether a field is an index field, an attribute or a summary field. This is a requirement if we
+ * hope to move to a model where index fields, attributes and summary fields share a common field class.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ValidateFieldTypes extends Processor {
+
+ public ValidateFieldTypes(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ String searchName = search.getName();
+ Map<String, DataType> fieldTypes = new HashMap<>();
+ for (SDField field : search.allFieldsList()) {
+ checkFieldType(searchName, "index field", field.getName(), field.getDataType(), fieldTypes);
+ for (Map.Entry<String, Attribute> entry : field.getAttributes().entrySet()) {
+ checkFieldType(searchName, "attribute", entry.getKey(), entry.getValue().getDataType(), fieldTypes);
+ }
+ }
+ for (DocumentSummary summary : search.getSummaries().values()) {
+ for (SummaryField field : summary.getSummaryFields()) {
+ checkFieldType(searchName, "summary field", field.getName(), field.getDataType(), fieldTypes);
+ }
+ }
+ }
+
+ private void checkFieldType(String searchName, String fieldDesc, String fieldName, DataType fieldType,
+ Map<String, DataType> fieldTypes)
+ {
+ DataType prevType = fieldTypes.get(fieldName);
+ if (prevType == null) {
+ fieldTypes.put(fieldName, fieldType);
+ } else if (!equalTypes(prevType, fieldType)) {
+ throw newProcessException(searchName, fieldName, "Duplicate field name with different types. Expected " + prevType.getName() + " for " + fieldDesc +
+ " '" + fieldName + "', got " + fieldType.getName() + ".");
+ }
+ }
+
+ private boolean equalTypes(DataType d1, DataType d2) {
+ if ("tag".equals(d1.getName())) {
+ return "tag".equals(d2.getName()) || "WeightedSet<string>".equals(d2.getName());
+ }
+ if ("tag".equals(d2.getName())) {
+ return "tag".equals(d1.getName()) || "WeightedSet<string>".equals(d1.getName());
+ }
+ return d1.equals(d2);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldWithIndexSettingsCreatesIndex.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldWithIndexSettingsCreatesIndex.java
new file mode 100644
index 00000000000..a5c7d25532d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/ValidateFieldWithIndexSettingsCreatesIndex.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.Ranking;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * Check that fields with index settings actually creates an index or attribute
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ValidateFieldWithIndexSettingsCreatesIndex extends Processor {
+
+ public ValidateFieldWithIndexSettingsCreatesIndex(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ Matching defaultMatching = new Matching();
+ Ranking defaultRanking = new Ranking();
+ for (SDField field : search.allFieldsList()) {
+ if (field.doesIndexing()) {
+ continue;
+ }
+ if (field.doesAttributing()) {
+ continue;
+ }
+ if (!(field.getRanking().equals(defaultRanking))) {
+ fail(search, field, "Fields which are not creating an index or attribute can not contain rank settings.");
+ }
+ if (!(field.getMatching().equals(defaultMatching))) {
+ fail(search, field, "Fields which are not creating an index or attribute can not contain match settings.");
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/WordMatch.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/WordMatch.java
new file mode 100644
index 00000000000..24b811fc7fc
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/WordMatch.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+/**
+ * The implementation of word matching - with word matching the field is assumed to contain a single "word" - some
+ * contiguous sequence of word and number characters - but without changing the data at the indexing side (as with text
+ * matching) to enforce this. Word matching is thus almost like exact matching on the indexing side (no action taken),
+ * and like text matching on the query side. This may be suitable for attributes, where people both expect the data to
+ * be left as in the input document, and trivially written queries to work by default. However, this may easily lead to
+ * data which cannot be matched at all as the indexing and query side does not agree.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class WordMatch extends Processor {
+
+ public WordMatch(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ public void process() {
+ for (SDField field : search.allFieldsList()) {
+ if (!field.getMatching().getType().equals(Matching.Type.WORD)) {
+ continue;
+ }
+ field.setStemming(Stemming.NONE);
+ field.getNormalizing().inferLowercase();
+ field.addQueryCommand("word");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/IndexCommandResolver.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/IndexCommandResolver.java
new file mode 100644
index 00000000000..158515fd05e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/IndexCommandResolver.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing.multifieldresolver;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * Resolver-class for harmonizing index-commands in multifield indexes
+ */
+public class IndexCommandResolver extends MultiFieldResolver {
+
+ /** Commands which don't have to be harmonized between fields */
+ private static List<String> ignoredCommands = new ArrayList<>();
+
+ /** Commands which must be harmonized between fields */
+ private static List<String> harmonizedCommands = new ArrayList<>();
+
+ static {
+ String[] ignore = { "complete-boost", "literal-boost", "highlight" };
+ ignoredCommands.addAll(Arrays.asList(ignore));
+ String[] harmonize = { "stemming", "normalizing" };
+ harmonizedCommands.addAll(Arrays.asList(harmonize));
+ }
+
+ public IndexCommandResolver(String indexName, List<SDField> fields, Search search, DeployLogger logger) {
+ super(indexName, fields, search, logger);
+ }
+
+ /**
+ * Check index-commands for each field, report and attempt to fix any
+ * inconsistencies
+ */
+ public void resolve() {
+ for (SDField field : fields) {
+ for (String command : field.getQueryCommands()) {
+ if (!ignoredCommands.contains(command))
+ checkCommand(command);
+ }
+ }
+ }
+
+ private void checkCommand(String command) {
+ for (SDField field : fields) {
+ if (!field.hasQueryCommand(command)) {
+ if (harmonizedCommands.contains(command)) {
+ deployLogger.log(Level.WARNING, command + " must be added to all fields going to the same index (" + indexName + ")" +
+ ", adding to field " + field.getName());
+ field.addQueryCommand(command);
+ } else {
+ deployLogger.log(Level.WARNING, "All fields going to the same index should have the same query-commands. Field \'" + field.getName() +
+ "\' doesn't contain command \'" + command+"\'");
+ }
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/MultiFieldResolver.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/MultiFieldResolver.java
new file mode 100644
index 00000000000..943a788fc75
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/MultiFieldResolver.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing.multifieldresolver;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+import java.util.List;
+
+/**
+ * Abstract superclass of all multifield conflict resolvers
+ */
+public abstract class MultiFieldResolver {
+
+ protected String indexName;
+ protected List<SDField> fields;
+ protected Search search;
+
+ protected DeployLogger deployLogger;
+
+ public MultiFieldResolver(String indexName, List<SDField> fields, Search search, DeployLogger logger) {
+ this.indexName = indexName;
+ this.fields = fields;
+ this.search = search;
+ this.deployLogger = logger;
+ }
+
+ /**
+ * Checks the list of fields for specific conflicts, and reports and/or
+ * attempts to correct them
+ */
+ public abstract void resolve();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java
new file mode 100644
index 00000000000..d98dd97ff83
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankProfileTypeSettingsProcessor.java
@@ -0,0 +1,85 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing.multifieldresolver;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.TensorFieldType;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.processing.Processor;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class that processes a search instance and sets type settings on all rank profiles.
+ *
+ * Currently, type settings are limited to the type of tensor attribute fields and tensor query features.
+ *
+ * @author <a href="geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class RankProfileTypeSettingsProcessor extends Processor {
+
+ private static final Pattern queryFeaturePattern = Pattern.compile("query\\((\\w+)\\)$");
+
+ public RankProfileTypeSettingsProcessor(Search search, DeployLogger deployLogger, RankProfileRegistry rankProfileRegistry, QueryProfiles queryProfiles) {
+ super(search, deployLogger, rankProfileRegistry, queryProfiles);
+ }
+
+ @Override
+ public void process() {
+ processAttributeFields();
+ processQueryProfileTypes();
+
+ }
+
+ private void processAttributeFields() {
+ for (SDField field : search.allFieldsList()) {
+ Attribute attribute = field.getAttributes().get(field.getName());
+ if (attribute != null && attribute.tensorType().isPresent()) {
+ addAttributeTypeToRankProfiles(attribute.getName(), attribute.tensorType().get().toString());
+ }
+ }
+ }
+
+ private void addAttributeTypeToRankProfiles(String attributeName, String attributeType) {
+ for (RankProfile profile : rankProfileRegistry.allRankProfiles()) {
+ profile.addAttributeType(attributeName, attributeType);
+ }
+ }
+
+ private void processQueryProfileTypes() {
+ for (QueryProfileType queryProfileType : queryProfiles.getRegistry().getTypeRegistry().allComponents()) {
+ for (Map.Entry<String, FieldDescription> fieldDescEntry : queryProfileType.fields().entrySet()) {
+ processFieldDescription(fieldDescEntry.getValue());
+ }
+ }
+ }
+
+ private void processFieldDescription(FieldDescription fieldDescription) {
+ String fieldName = fieldDescription.getName();
+ FieldType fieldType = fieldDescription.getType();
+ if (fieldType instanceof TensorFieldType) {
+ TensorFieldType tensorFieldType = (TensorFieldType)fieldType;
+ Matcher matcher = queryFeaturePattern.matcher(fieldName);
+ if (tensorFieldType.type().isPresent() && matcher.matches()) {
+ String queryFeature = matcher.group(1);
+ addQueryFeatureTypeToRankProfiles(queryFeature, tensorFieldType.type().get().toString());
+ }
+ }
+ }
+
+ private void addQueryFeatureTypeToRankProfiles(String queryFeature, String queryFeatureType) {
+ for (RankProfile profile : rankProfileRegistry.allRankProfiles()) {
+ profile.addQueryFeatureType(queryFeature, queryFeatureType);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankTypeResolver.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankTypeResolver.java
new file mode 100644
index 00000000000..787a70862a9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/RankTypeResolver.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing.multifieldresolver;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Search;
+
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * Checks if fields have defined different rank types for the same
+ * index (typically in an index-to statement), and if they have
+ * output a warning and use the first ranktype.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class RankTypeResolver extends MultiFieldResolver {
+
+ public RankTypeResolver(String indexName, List<SDField> fields, Search search, DeployLogger logger) {
+ super(indexName, fields, search, logger);
+ }
+
+ public void resolve() {
+ RankType rankType = null;
+ if (fields.size() > 0) {
+ boolean first = true;
+ for (SDField field : fields) {
+ if (first) {
+ rankType = fields.get(0).getRankType();
+ first = false;
+ } else if (!field.getRankType().equals(rankType)) {
+ deployLogger.log(Level.WARNING, "In field '" + field.getName() + "' " +
+ field.getRankType() + " for index '" + indexName +
+ "' conflicts with " + rankType +
+ " defined for the same index in field '" +
+ field.getName() + "'. Using " +
+ rankType + ".");
+ field.setRankType(rankType);
+ }
+ }
+ }
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/StemmingResolver.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/StemmingResolver.java
new file mode 100644
index 00000000000..f1e1899391d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/multifieldresolver/StemmingResolver.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing.multifieldresolver;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.language.Linguistics;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.searchdefinition.processing.BuiltInFieldSets;
+import com.yahoo.vespa.indexinglanguage.expressions.*;
+import com.yahoo.vespa.indexinglanguage.linguistics.AnnotatorConfig;
+
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * Class resolving conflicts when fields with different stemming-settings are
+ * combined into the same index
+ */
+public class StemmingResolver extends MultiFieldResolver {
+
+ public StemmingResolver(String indexName, List<SDField> fields, Search search, DeployLogger logger) {
+ super(indexName, fields, search, logger);
+ }
+
+ @Override
+ public void resolve() {
+ checkStemmingForIndexFields(indexName, fields);
+ }
+
+ private void checkStemmingForIndexFields(String indexName, List<SDField> fields) {
+ Stemming stemming = null;
+ SDField stemmingField = null;
+ for (SDField field : fields) {
+ if (stemming == null && stemmingField==null) {
+ stemming = field.getStemming(search);
+ stemmingField = field;
+ } else if (stemming != field.getStemming(search)) {
+ deployLogger.log(Level.WARNING, "Field '" + field.getName() + "' has " + field.getStemming(search) +
+ ", whereas field '" + stemmingField.getName() + "' has " + stemming +
+ ". All fields indexing to the index '" + indexName + "' must have the same stemming." +
+ " This should be corrected as it will make indexing fail in a few cases.");
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/searchdefinition/processing/package-info.java b/config-model/src/main/java/com/yahoo/searchdefinition/processing/package-info.java
new file mode 100644
index 00000000000..9915e11b28d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/searchdefinition/processing/package-info.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Classes in this package (processors) implements some search
+ * definition features by reducing them to simpler features.
+ * The processors are run after parsing of the search definition,
+ * before creating the derived model.
+ *
+ * For simplicity, features should always be implemented here
+ * rather than in the derived model if possible.
+ *
+ * New processors must be added to the list in Processing.
+ */
+@com.yahoo.api.annotations.PackageMarker
+package com.yahoo.searchdefinition.processing;
diff --git a/config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentManager.java b/config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentManager.java
new file mode 100644
index 00000000000..29c0c4e8136
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentManager.java
@@ -0,0 +1,156 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.configmodel.producers;
+
+import com.yahoo.document.config.DocumentmanagerConfig;
+import static com.yahoo.document.config.DocumentmanagerConfig.*;
+import com.yahoo.document.*;
+import com.yahoo.document.annotation.AnnotationReferenceDataType;
+import com.yahoo.document.annotation.AnnotationType;
+import com.yahoo.documentmodel.DataTypeCollection;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.documentmodel.VespaDocumentType;
+import com.yahoo.searchdefinition.document.FieldSet;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author balder
+ * @since 2010-02-19
+ */
+public class DocumentManager {
+
+ public DocumentmanagerConfig.Builder produce(DocumentModel model, DocumentmanagerConfig.Builder docman) {
+ docman.enablecompression(false);
+ Set<DataType> handled = new HashSet<>();
+ for(NewDocumentType documentType : model.getDocumentManager().getTypes()) {
+ handle(documentType, docman, handled);
+ handleAnnotations(documentType.getAnnotations(), docman);
+ if ( documentType != VespaDocumentType.INSTANCE) {
+ DocumentmanagerConfig.Datatype.Builder dt = new DocumentmanagerConfig.Datatype.Builder();
+ docman.datatype(dt);
+ handleDataType(documentType, dt);
+ }
+ }
+ return docman;
+ }
+
+ private void handle(DataTypeCollection type, DocumentmanagerConfig.Builder docman, Set<DataType> handled) {
+ for (DataType dataType : type.getTypes()) {
+ if (handled.contains(dataType)) continue;
+ handled.add(dataType);
+ if (dataType instanceof TemporaryStructuredDataType) continue;
+ if ((dataType.getId() < 0) || (DataType.lastPredefinedDataTypeId() < dataType.getId())) {
+ Datatype.Builder dtc = new Datatype.Builder();
+ docman.datatype(dtc);
+ handleDataType(dataType, dtc);
+ }
+ }
+ }
+
+ private void handleAnnotation(AnnotationType type, DocumentmanagerConfig.Annotationtype.Builder atb) {
+ atb.
+ id(type.getId()).
+ name(type.getName());
+ if (type.getDataType() != null) {
+ atb.datatype(type.getDataType().getId());
+ }
+ if ( ! type.getInheritedTypes().isEmpty()) {
+ for (AnnotationType inherited : type.getInheritedTypes()) {
+ atb.inherits(new DocumentmanagerConfig.Annotationtype.Inherits.Builder().id(inherited.getId()));
+ }
+ }
+ }
+ private void handleAnnotations(Collection<AnnotationType> types, DocumentmanagerConfig.Builder builder) {
+ for (AnnotationType type : types) {
+ DocumentmanagerConfig.Annotationtype.Builder atb = new DocumentmanagerConfig.Annotationtype.Builder();
+ handleAnnotation(type, atb);
+ builder.annotationtype(atb);
+ }
+ }
+
+ private void handleDataType(DataType type, Datatype.Builder dtc) {
+ dtc.id(type.getId());
+ if (type instanceof ArrayDataType) {
+ CollectionDataType dt = (CollectionDataType) type;
+ dtc.arraytype(new Datatype.Arraytype.Builder().datatype(dt.getNestedType().getId()));
+ } else if (type instanceof WeightedSetDataType) {
+ WeightedSetDataType dt = (WeightedSetDataType) type;
+ dtc.weightedsettype(new Datatype.Weightedsettype.Builder().
+ datatype(dt.getNestedType().getId()).
+ createifnonexistant(dt.createIfNonExistent()).
+ removeifzero(dt.removeIfZero()));
+ } else if (type instanceof MapDataType) {
+ MapDataType mtype = (MapDataType) type;
+ dtc.maptype(new Datatype.Maptype.Builder().
+ keytype(mtype.getKeyType().getId()).
+ valtype(mtype.getValueType().getId()));
+ } else if (type instanceof DocumentType) {
+ DocumentType dt = (DocumentType) type;
+ Datatype.Documenttype.Builder doc = new Datatype.Documenttype.Builder();
+ dtc.documenttype(doc);
+ doc.
+ name(dt.getName()).
+ headerstruct(dt.getHeaderType().getId()).
+ bodystruct(dt.getBodyType().getId());
+ for (DocumentType inherited : dt.getInheritedTypes()) {
+ doc.inherits(new Datatype.Documenttype.Inherits.Builder().name(inherited.getName()));
+ }
+ } else if (type instanceof NewDocumentType) {
+ NewDocumentType dt = (NewDocumentType) type;
+ Datatype.Documenttype.Builder doc = new Datatype.Documenttype.Builder();
+ dtc.documenttype(doc);
+ doc.
+ name(dt.getName()).
+ headerstruct(dt.getHeader().getId()).
+ bodystruct(dt.getBody().getId());
+ for (NewDocumentType inherited : dt.getInherited()) {
+ doc.inherits(new Datatype.Documenttype.Inherits.Builder().name(inherited.getName()));
+ }
+ handleFieldSets(dt.getFieldSets(), doc);
+ } else if (type instanceof TemporaryStructuredDataType) {
+ //Ignored
+ } else if (type instanceof StructDataType) {
+ StructDataType dt = (StructDataType) type;
+ Datatype.Structtype.Builder st = new Datatype.Structtype.Builder();
+ dtc.structtype(st);
+ st.name(dt.getName());
+ if (dt.getCompressionConfig().type.getCode() != 0) {
+ st.
+ compresstype(Datatype.Structtype.Compresstype.Enum.valueOf(dt.getCompressionConfig().type.toString())).
+ compresslevel(dt.getCompressionConfig().compressionLevel).
+ compressthreshold((int)dt.getCompressionConfig().threshold).
+ compressminsize((int)dt.getCompressionConfig().minsize);
+ }
+ for (com.yahoo.document.Field field : dt.getFieldsThisTypeOnly()) {
+ Datatype.Structtype.Field.Builder fb = new Datatype.Structtype.Field.Builder();
+ st.field(fb);
+ fb.name(field.getName());
+ if (field.hasForcedId()) {
+ fb.id(new Datatype.Structtype.Field.Id.Builder().id(field.getId()));
+ }
+ fb.datatype(field.getDataType().getId());
+ }
+ for (StructDataType inherited : dt.getInheritedTypes()) {
+ st.inherits(new Datatype.Structtype.Inherits.Builder().name(inherited.getName()));
+ }
+ } else if (type instanceof AnnotationReferenceDataType) {
+ AnnotationReferenceDataType annotationRef = (AnnotationReferenceDataType) type;
+ dtc.annotationreftype(new Datatype.Annotationreftype.Builder().annotation(annotationRef.getAnnotationType().getName()));
+ } else {
+ throw new IllegalArgumentException("Can not handle datatype '" + type.getName());
+ }
+ }
+
+ private void handleFieldSets(Set<FieldSet> fieldSets, Datatype.Documenttype.Builder doc) {
+
+ for (FieldSet builtinFs : fieldSets) {
+ handleFieldSet(builtinFs, doc);
+ }
+ }
+
+ private void handleFieldSet(FieldSet fs, Datatype.Documenttype.Builder doc) {
+ doc.fieldsets(fs.getName(), new Datatype.Documenttype.Fieldsets.Builder().fields(fs.getFieldNames()));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentTypes.java b/config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentTypes.java
new file mode 100644
index 00000000000..92b3c13d383
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/configmodel/producers/DocumentTypes.java
@@ -0,0 +1,158 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.configmodel.producers;
+
+import com.yahoo.document.*;
+import com.yahoo.document.DocumenttypesConfig.Builder;
+import com.yahoo.document.annotation.AnnotationReferenceDataType;
+import com.yahoo.document.annotation.AnnotationType;
+import com.yahoo.documentmodel.DataTypeCollection;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.documentmodel.VespaDocumentType;
+import com.yahoo.searchdefinition.FieldSets;
+import com.yahoo.searchdefinition.document.FieldSet;
+import com.yahoo.vespa.documentmodel.DocumentModel;
+import java.util.*;
+
+/**
+ * @author balder
+ */
+public class DocumentTypes {
+
+ public DocumenttypesConfig.Builder produce(DocumentModel model, DocumenttypesConfig.Builder builder) {
+ Map<NewDocumentType.Name, NewDocumentType> produced = new HashMap<>();
+ for(NewDocumentType documentType : model.getDocumentManager().getTypes()) {
+ produceInheritOrder(documentType, builder, produced);
+ }
+ return builder;
+ }
+
+ private void produceInheritOrder(NewDocumentType documentType, DocumenttypesConfig.Builder builder, Map<NewDocumentType.Name, NewDocumentType> produced) {
+ if ( ! produced.containsKey(documentType.getFullName())) {
+ for (NewDocumentType inherited : documentType.getInherited()) {
+ produceInheritOrder(inherited, builder, produced);
+ }
+ handle(documentType, builder);
+ produced.put(documentType.getFullName(), documentType);
+ }
+ }
+
+ private void handle(NewDocumentType documentType, DocumenttypesConfig.Builder builder) {
+ if (documentType == VespaDocumentType.INSTANCE) {
+ return;
+ }
+ DocumenttypesConfig.Documenttype.Builder db = new DocumenttypesConfig.Documenttype.Builder();
+ db.
+ id(documentType.getId()).
+ name(documentType.getName()).
+ headerstruct(documentType.getHeader().getId()).
+ bodystruct(documentType.getBody().getId());
+ Set<Integer> handled = new HashSet<>();
+ for (NewDocumentType inherited : documentType.getInherited()) {
+ db.inherits(new DocumenttypesConfig.Documenttype.Inherits.Builder().id(inherited.getId()));
+ markAsHandled(handled, inherited.getAllTypes());
+ }
+ for (DataType dt : documentType.getTypes()) {
+ handle(dt, db, handled);
+ }
+ for(AnnotationType annotation : documentType.getAnnotations()) {
+ DocumenttypesConfig.Documenttype.Annotationtype.Builder atb = new DocumenttypesConfig.Documenttype.Annotationtype.Builder();
+ db.annotationtype(atb);
+ handle(annotation, atb);
+ }
+ handleFieldSets(documentType.getFieldSets(), db);
+ builder.documenttype(db);
+ }
+
+ private void handleFieldSets(Set<FieldSet> fieldSets, com.yahoo.document.DocumenttypesConfig.Documenttype.Builder db) {
+ for (FieldSet fs : fieldSets) {
+ handleFieldSet(fs, db);
+ }
+ }
+
+ private void handleFieldSet(FieldSet fs, DocumenttypesConfig.Documenttype.Builder db) {
+ db.fieldsets(fs.getName(), new DocumenttypesConfig.Documenttype.Fieldsets.Builder().fields(fs.getFieldNames()));
+ }
+
+ private void markAsHandled(Set<Integer> handled, DataTypeCollection typeCollection) {
+ for (DataType type : typeCollection.getTypes()) {
+ handled.add(type.getId());
+ }
+ }
+
+ private void handle(AnnotationType annotation, DocumenttypesConfig.Documenttype.Annotationtype.Builder builder) {
+ builder.
+ id(annotation.getId()).
+ name(annotation.getName());
+ DataType dt = annotation.getDataType();
+ if (dt!=null) {
+ builder.datatype(dt.getId());
+ }
+ for (AnnotationType inherited : annotation.getInheritedTypes()) {
+ builder.inherits(new DocumenttypesConfig.Documenttype.Annotationtype.Inherits.Builder().id(inherited.getId()));
+ }
+ }
+
+ private void handle(DataType type, DocumenttypesConfig.Documenttype.Builder db, Set<Integer> handled) {
+ if ((VespaDocumentType.INSTANCE.getDataType(type.getId()) == null) && ! handled.contains(type.getId())) {
+ handled.add(type.getId());
+ DocumenttypesConfig.Documenttype.Datatype.Builder dtb = new DocumenttypesConfig.Documenttype.Datatype.Builder();
+ dtb.id(type.getId());
+ if (type instanceof StructDataType) {
+ dtb.type(DocumenttypesConfig.Documenttype.Datatype.Type.Enum.valueOf("STRUCT"));
+ StructDataType dt = (StructDataType) type;
+ DocumenttypesConfig.Documenttype.Datatype.Sstruct.Builder sb = new DocumenttypesConfig.Documenttype.Datatype.Sstruct.Builder();
+ dtb.sstruct(sb);
+ sb.name(dt.getName());
+ if (dt.getCompressionConfig().type.getCode() != 0) {
+ sb.compression(new DocumenttypesConfig.Documenttype.Datatype.Sstruct.Compression.Builder().
+ type(DocumenttypesConfig.Documenttype.Datatype.Sstruct.Compression.Type.Enum.valueOf(dt.getCompressionConfig().type.toString())).
+ level(dt.getCompressionConfig().compressionLevel).
+ threshold((int)dt.getCompressionConfig().threshold).
+ minsize((int)dt.getCompressionConfig().minsize));
+ }
+ for (com.yahoo.document.Field field : dt.getFields()) {
+ sb.field(new DocumenttypesConfig.Documenttype.Datatype.Sstruct.Field.Builder().
+ name(field.getName()).
+ id(field.getId()).
+ id_v6(field.getIdV6()).
+ datatype(field.getDataType().getId()));
+ handle(field.getDataType(), db, handled);
+ }
+ } else if (type instanceof ArrayDataType) {
+ dtb.
+ type(DocumenttypesConfig.Documenttype.Datatype.Type.Enum.valueOf("ARRAY")).
+ array(new DocumenttypesConfig.Documenttype.Datatype.Array.Builder().
+ element(new DocumenttypesConfig.Documenttype.Datatype.Array.Element.Builder().id(((ArrayDataType)type).getNestedType().getId())));
+ handle(((ArrayDataType)type).getNestedType(), db, handled);
+ } else if (type instanceof WeightedSetDataType) {
+ dtb.type(DocumenttypesConfig.Documenttype.Datatype.Type.Enum.valueOf("WSET")).
+ wset(new DocumenttypesConfig.Documenttype.Datatype.Wset.Builder().
+ key(new DocumenttypesConfig.Documenttype.Datatype.Wset.Key.Builder().
+ id(((WeightedSetDataType)type).getNestedType().getId())).
+ createifnonexistent(((WeightedSetDataType)type).createIfNonExistent()).
+ removeifzero(((WeightedSetDataType)type).removeIfZero()));
+ handle(((WeightedSetDataType)type).getNestedType(), db, handled);
+ } else if (type instanceof MapDataType) {
+ dtb.
+ type(DocumenttypesConfig.Documenttype.Datatype.Type.Enum.valueOf("MAP")).
+ map(new DocumenttypesConfig.Documenttype.Datatype.Map.Builder().
+ key(new DocumenttypesConfig.Documenttype.Datatype.Map.Key.Builder().
+ id(((MapDataType)type).getKeyType().getId())).
+ value(new DocumenttypesConfig.Documenttype.Datatype.Map.Value.Builder().
+ id(((MapDataType)type).getValueType().getId())));
+ handle(((MapDataType)type).getKeyType(), db, handled);
+ handle(((MapDataType)type).getValueType(), db, handled);
+ } else if (type instanceof AnnotationReferenceDataType) {
+ dtb.
+ type(DocumenttypesConfig.Documenttype.Datatype.Type.Enum.valueOf("ANNOTATIONREF")).
+ annotationref(new DocumenttypesConfig.Documenttype.Datatype.Annotationref.Builder().
+ annotation(new DocumenttypesConfig.Documenttype.Datatype.Annotationref.Annotation.Builder().
+ id(((AnnotationReferenceDataType)type).getAnnotationType().getId())));
+ } else {
+ return;
+ }
+ db.datatype(dtb);
+ }
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentModel.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentModel.java
new file mode 100644
index 00000000000..6c8206d30f2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentModel.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import com.yahoo.documentmodel.DocumentTypeRepo;
+
+/**
+ * DocumentModel represents everything derived from a set of search definitions.
+ * It contains a document manager managing all defined document types.
+ * It contains a search manager managing all specified search definitions.
+ * It contains a storage manager managing all specified storage definitions.
+ *
+ * @author balder
+ * @since 2010-02-19
+ */
+public class DocumentModel {
+ private DocumentTypeRepo documentMan = new DocumentTypeRepo();
+ private SearchManager searchMan = new SearchManager();
+
+ /**
+ *
+ * @return Returns the DocumentManager
+ */
+ public DocumentTypeRepo getDocumentManager() { return documentMan; }
+
+ /**
+ *
+ * @return Returns the SearchManager
+ */
+ public SearchManager getSearchManager() { return searchMan; }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentSummary.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentSummary.java
new file mode 100644
index 00000000000..42fa7b04cf6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/DocumentSummary.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import com.yahoo.document.Field;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A document summary definition - a list of summary fields.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class DocumentSummary extends FieldView {
+
+
+ /**
+ * Will create a DocumentSummary with the given name.
+ * @param name The name to use for this summary.
+ */
+ public DocumentSummary(String name) {
+ super(name);
+ }
+
+ /**
+ * The model is constrained to ensure that summary fields of the same name
+ * in different classes have the same summary transform, because this is
+ * what is supported by the backend currently.
+ * @param summaryField The summaryfield to add
+ */
+ public void add(SummaryField summaryField) {
+ summaryField.addDestination(getName());
+ super.add(summaryField);
+ }
+
+ public SummaryField getSummaryField(String name) {
+ return (SummaryField) get(name);
+ }
+
+ public Collection<SummaryField> getSummaryFields() {
+ ArrayList<SummaryField> fields = new ArrayList<>(getFields().size());
+ for(Field f : getFields()) {
+ fields.add((SummaryField) f);
+ }
+ return fields;
+ }
+
+ /**
+ * Removes implicit fields which shouldn't be included.
+ * This is implicitly added fields which are sources for
+ * other fields. We then assume they are not intended to be added
+ * implicitly in additon.
+ * This should be called when this summary is complete.
+ */
+ public void purgeImplicits() {
+ List<SummaryField> falseImplicits = new ArrayList<>();
+ for (SummaryField summaryField : getSummaryFields() ) {
+ if (summaryField.isImplicit()) continue;
+ for (Iterator<SummaryField.Source> j = summaryField.sourceIterator(); j.hasNext(); ) {
+ String sourceName = j.next().getName();
+ if (sourceName.equals(summaryField.getName())) continue;
+ SummaryField sourceField=getSummaryField(sourceName);
+ if (sourceField==null) continue;
+ if (!sourceField.isImplicit()) continue;
+ falseImplicits.add(sourceField);
+ }
+ }
+ for (SummaryField field : falseImplicits) {
+ remove(field.getName());
+ }
+ }
+
+ public String toString() {
+ return "document summary '" + getName() + "'";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/FieldView.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/FieldView.java
new file mode 100644
index 00000000000..dfb44aef917
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/FieldView.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import com.yahoo.document.Field;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author balder
+ * @since 2010-02-19
+ */
+public class FieldView implements Serializable {
+ private String name;
+ private Map<String, Field> fields = new LinkedHashMap<>();
+
+ /**
+ * Creates a view with a name
+ * @param name Name of the view.
+ */
+ public FieldView(String name) {
+ this.name = name;
+ }
+ public String getName() { return name; }
+ public Collection<Field> getFields() { return fields.values(); }
+ public Field get(String name) { return fields.get(name); }
+ public void remove(String name) { fields.remove(name); }
+
+ /**
+ * This method will add a field to a view. All fields must come from the same document type. Not enforced here.
+ * @param field The field to add.
+ * @return Itself for chaining purposes.
+ */
+ public FieldView add(Field field) {
+ if (fields.containsKey(field.getName())) {
+ if ( ! fields.get(field.getName()).equals(field)) {
+ throw new IllegalArgumentException(
+ "View '" + name + "' already contains a field with name '" +
+ field.getName() + "' and definition : " +
+ fields.get(field.getName()).toString() + ". Your is : " + field.toString());
+ }
+ } else {
+ fields.put(field.getName(), field);
+ }
+ return this;
+ }
+
+ /**
+ * This method will join the two views.
+ * @param view The view to be joined in to this.
+ * @return Itself for chaining.
+ */
+ public FieldView add(FieldView view) {
+ for(Field field : view.getFields()) {
+ add(field);
+ }
+ return this;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchDef.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchDef.java
new file mode 100644
index 00000000000..07b7c973841
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchDef.java
@@ -0,0 +1,126 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentTypeManager;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * @author balder
+ * @since 2010-02-19
+ */
+public class SearchDef {
+ private final static Logger log = Logger.getLogger(SearchDef.class.getName());
+ /// Name of the searchdefinition
+ private String name;
+ /// These are the real backing documenttypes
+ private DocumentTypeManager sources = new DocumentTypeManager();
+ /// Map of all search fields
+ private Map<String, SearchField> fields = new HashMap<>();
+ /// Map of all views that can be searched.
+ private Map<String, FieldView> views = new HashMap<>();
+ /// Map of all aliases <alias, realname>
+ private Map<String, String> aliases = new HashMap<>();
+
+ /**
+ * Will create a SearchDef with the given name
+ * @param name The name of the searchdefinition
+ */
+ public SearchDef(String name) {
+ this.name = name;
+ }
+
+ /**
+ * This will provide you with the name of the searchdefinition.
+ * @return The name of the searchdefinition.
+ */
+ public String getName() { return name; }
+
+ public Map<String, SearchField> getFields() { return fields; }
+ public Map<String, FieldView> getViews() { return views; }
+
+ /**
+ * Adds a document that can be mapped to this search.
+ * @param source A document that can be mapped to this search.
+ * @return Itself for chaining.
+ */
+ public SearchDef add(DataType source) {
+ sources.register(source);
+ return this;
+ }
+
+ private void noShadowing(String name) {
+ noFieldShadowing(name);
+ noViewShadowing(name);
+ }
+
+ private void noFieldShadowing(String name) {
+ if (fields.containsKey(name)) {
+ throw new IllegalArgumentException("Searchdef '" + getName() + "' already contains the fields '" + fields.toString() +
+ "'. You are trying to add '" + name + "'. Shadowing is not supported");
+ }
+ }
+
+ private void noViewShadowing(String name) {
+ if (views.containsKey(name)) {
+ throw new IllegalArgumentException("Searchdef '" + getName() + "' already contains a view with name '" +
+ name + "'. Shadowing is not supported.");
+ }
+ }
+
+ /**
+ * Adds a search field to the definition.
+ * @param field The field to add.
+ * @return Itself for chaining.
+ */
+ public SearchDef add(SearchField field) {
+ try {
+ noFieldShadowing(field.getName());
+ fields.put(field.getName(), field);
+ } catch (IllegalArgumentException e) {
+ if (views.containsKey(field.getName())) {
+ throw e;
+ }
+ }
+ return this;
+ }
+
+ public SearchDef addAlias(String alias, String aliased) {
+ noShadowing(alias);
+ if (!fields.containsKey(aliased) && !views.containsKey(aliased)) {
+ if (aliased.contains(".")) {
+ // TODO Here we should nest ourself down to something that really exists.
+ log.warning("Aliased item '" + aliased + "' not verifiable. Allowing it to be aliased to '" + alias + " for now. Validation will come when URL/Position is structified.");
+ } else {
+ throw new IllegalArgumentException("Searchdef '" + getName() + "' has nothing named '" + aliased + "'to alias to '" + alias + "'.");
+ }
+ }
+ String oldAliased = aliases.get(alias);
+ if ((oldAliased != null)) {
+ if (oldAliased.equals(aliased)) {
+ throw new IllegalArgumentException("Searchdef '" + getName() + "' already has the alias '" + alias +
+ "' to '" + aliased + ". Why do you want to add it again.");
+
+ } else {
+ throw new IllegalArgumentException("Searchdef '" + getName() + "' already has the alias '" + alias +
+ "' to '" + oldAliased + ". Cannot change it to alias '" + aliased + "'.");
+ }
+ } else {
+ aliases.put(alias, aliased);
+ }
+ return this;
+ }
+
+ public SearchDef add(FieldView view) {
+ noViewShadowing(view.getName());
+ if (views.containsKey(view.getName())) {
+ views.get(view.getName()).add(view);
+ }
+ views.put(view.getName(), view);
+ return this;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchField.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchField.java
new file mode 100644
index 00000000000..2db81861955
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchField.java
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author balder
+ * @since 2010-02-19
+ */
+public class SearchField extends Field {
+ /// Indicate if field shall be stored in memory for attribute usage.
+ private boolean attribute = false;
+ /// Indicate if the field is Vespa indexed.
+ private boolean indexed = false;
+ /// Indication to backend on how much optimization should be done.
+
+ /**
+ * This is a representation of features to generate for this field.
+ * It can be both optimize hints, and real functional hints.
+ */
+ public enum Feature {
+ WEIGHT_IN_ATTRIBUTE_POSTINGS("WeightInAttributePosting"), // Hint to put the weight in postings for attribute.
+ WORDPOS_IN_POSTINGS("WordPosInPosting"), // Default for generating posocc
+ FILTER_ONLY("FilterOnly"); // Might only generate bitvector
+ private String name;
+ Feature(String name) { this.name = name;}
+ public String getName() { return name; }
+ }
+ private List<Feature> featureList = new ArrayList<>();
+
+ public SearchField(Field field, boolean indexed, boolean attribute) {
+ this(field, indexed, attribute, null);
+ }
+ public SearchField(Field field, boolean indexed, boolean attribute, List<Feature> features) {
+ super(field.getName(), field);
+ this.attribute = attribute;
+ this.indexed = indexed;
+ if (features != null) {
+ featureList.addAll(features);
+ }
+ validate();
+ }
+
+ @SuppressWarnings({ "deprecation" })
+ private void validate() {
+ if (attribute || !indexed) {
+ return;
+ }
+ DataType fieldType = getDataType();
+ DataType primiType = fieldType.getPrimitiveType();
+ if (DataType.STRING.equals(primiType) || DataType.URI.equals(primiType)) {
+ return;
+ }
+ throw new IllegalStateException("Expected type " + DataType.STRING.getName() + " for indexed field '" +
+ getName() + "', got " + fieldType.getName() + ".");
+ }
+
+ public SearchField setIndexed() { indexed = true; validate(); return this; }
+ public SearchField setAttribute() { attribute = true; validate(); return this; }
+ public boolean isAttribute() { return attribute; }
+ /**
+ * True if field is Vespa indexed
+ * @return true if indexed
+ */
+ public boolean isIndexed() { return indexed; }
+ public SearchField addFeature(Feature feature) { featureList.add(feature); validate(); return this; }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchManager.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchManager.java
new file mode 100644
index 00000000000..29a960f7e7b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SearchManager.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import java.util.TreeMap;
+
+/**
+ * @author balder
+ * @since 2010-02-19
+ */
+public class SearchManager {
+ /// This is the list of all known search definitions
+ private TreeMap<String, SearchDef> defs = new TreeMap<>();
+
+ /**
+ * This will add a searchdefinition or throw an IllegalArgumentException if the name is already used
+ * @param def The searchdef to add
+ * @return itself for chaining purposes.
+ */
+ public SearchManager add(SearchDef def) {
+ if (defs.containsKey(def.getName())) {
+ throw new IllegalArgumentException("There already exist a searchdefinition with this content:\n" +
+ defs.get(def.getName()).toString() + "\n No room for : " + def.toString());
+ }
+ defs.put(def.getName(), def);
+ return this;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryField.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryField.java
new file mode 100644
index 00000000000..f6db82785b0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryField.java
@@ -0,0 +1,350 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.searchdefinition.document.TypedKey;
+
+import java.io.Serializable;
+import java.util.*;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * A summary field
+ *
+ * @author bratseth
+ */
+public class SummaryField extends Field implements Cloneable, TypedKey {
+
+ /**
+ * This class represents a source (field name) and the type of the source (only used for smart summary)
+ */
+ public static class Source implements Serializable {
+ public static enum Type {
+ CONTEXTUAL("contextual"),
+ TITLE("title"),
+ STATIC("static"),
+ URL("url");
+ private final String name;
+ Type(String name) {
+ this.name = name;
+ }
+ public String getName() { return name; }
+ }
+ private String name;
+ private Type type;
+ private boolean override = false;
+ public Source(String name) {
+ this.name = name;
+ this.type = Type.CONTEXTUAL;
+ }
+ public Source(String name, Type type) {
+ this.name = name;
+ this.type = type;
+ }
+ public String getName() { return name; }
+ public Type getType() { return type; }
+ public void setOverride(boolean override) { this.override = override; }
+ public boolean getOverride() { return override; }
+ public int hashCode() {
+ return name.hashCode() + type.getName().hashCode() + Boolean.valueOf(override).hashCode();
+ }
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Source)) {
+ return false;
+ }
+ Source other = (Source)obj;
+ return name.equals(other.name) &&
+ type.getName().equals(other.type.getName()) &&
+ override == other.override;
+ }
+ public String toString() {
+ return name;
+ }
+ }
+
+ /** A name-value property (used for smart summary) */
+ public static class Property implements Serializable {
+ private String name;
+ private String value;
+ public Property(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+ public String getName() { return name; }
+ public String getValue() { return value; }
+ public @Override int hashCode() {
+ return name.hashCode() + 17*value.hashCode();
+ }
+ public @Override boolean equals(Object obj) {
+ if (!(obj instanceof Property)) {
+ return false;
+ }
+ Property other = (Property)obj;
+ return name.equals(other.name) && value.equals(other.value);
+ }
+ }
+
+ /** The transform to perform on the stored source */
+ private SummaryTransform transform=SummaryTransform.NONE;
+
+ /** The command used per field in vsmsummary */
+ private VsmCommand vsmCommand = VsmCommand.NONE;
+
+ /**
+ * The data sources for this output summary field, in prioritized order
+ * (use only second source if first yields no result after transformation
+ * and so on). If no sources are given explicitly, the field of the same
+ * name as this summary field is used
+ */
+ private Set<Source> sources = new java.util.LinkedHashSet<>();
+
+ private Set<String> destinations=new java.util.LinkedHashSet<>();
+
+ /** True if this field was defined implicitly */
+ private boolean implicit=false;
+
+ /** The list of properties for this summary field */
+ private List<Property> properties = new ArrayList<>();
+
+ /** Creates a summary field with NONE as transform */
+ public SummaryField(String name, DataType type) {
+ this(name,type,SummaryTransform.NONE);
+ }
+
+ /** Creates a summary field with NONE as transform */
+ public SummaryField(Field field) {
+ this(field,SummaryTransform.NONE);
+ }
+
+
+ public SummaryField(Field field,SummaryTransform transform) {
+ this(field.getName(), field.getDataType(), transform);
+ }
+
+ public SummaryField(String name,DataType type,SummaryTransform transform) {
+ super(name, type);
+ this.transform=transform;
+ }
+
+ public void setImplicit(boolean implicit) { this.implicit=implicit; }
+
+ @Override // override to make public
+ public void setDataType(DataType type) {
+ super.setDataType(type);
+ }
+
+ public boolean isImplicit() { return implicit; }
+
+ public void setTransform(SummaryTransform transform) {
+ this.transform=transform;
+ if (SummaryTransform.DYNAMICTEASER.equals(transform) || SummaryTransform.BOLDED.equals(transform)) {
+ // This is the kind of logic we want to have in processing,
+ // but can't because of deriveDocuments mode, which doesn't run
+ // processing.
+ setVsmCommand(VsmCommand.FLATTENJUNIPER);
+ }
+ }
+
+ public SummaryTransform getTransform() { return transform; }
+
+ /** Returns the first source field of this, or null if the source field is not present */
+ public String getSourceField() {
+ String sourceName=getName();
+ if (sources.size()>0)
+ sourceName=sources.iterator().next().getName();
+ return sourceName;
+ }
+
+ public void addSource(String name) {
+ sources.add(new Source(name));
+ }
+
+ public void addSource(Source source) {
+ sources.add(source);
+ }
+
+ public Iterator<Source> sourceIterator() {
+ return sources.iterator();
+ }
+
+ public int getSourceCount() {
+ return sources.size();
+ }
+
+ /** Returns a modifiable set of the sources of this */
+ public Set<Source> getSources() { return sources; }
+
+ /** Returns the first source name of this, or the field name if no source has been set */
+ public String getSingleSource() {
+ if (sources.size()==0) return getName();
+ return sources.iterator().next().getName();
+ }
+
+ public void addDestination(String name) {
+ destinations.add(name);
+ }
+
+ public final void addDestinations(Iterable<String> names) {
+ for (String name : names) {
+ addDestination(name);
+ }
+ }
+
+ /** Returns an modifiable view of the destination set owned by this */
+ public Set<String> getDestinations() {
+ return destinations;
+ }
+
+ private String toString(Collection<?> collection) {
+ StringBuffer buffer=new StringBuffer();
+ for (Iterator<?> i=collection.iterator(); i.hasNext(); ) {
+ buffer.append(i.next().toString());
+ if (i.hasNext())
+ buffer.append(", ");
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Returns a summary field which merges the settings in the given field
+ * into this field
+ *
+ * @param merge the field to merge with this, if null, the merged field is
+ * <code>this</code> field
+ * @throws RuntimeException if the two fields can not be merged
+ */
+ public SummaryField mergeWith(SummaryField merge) {
+ if (merge==null) return this;
+ if (this.isImplicit()) return merge;
+ if (merge.isImplicit()) return this;
+
+ if (!merge.getName().equals(getName()))
+ throw new IllegalArgumentException(merge + " conflicts with " + this +
+ ": different names");
+
+ if (!merge.getTransform().equals(getTransform()))
+ throw new IllegalArgumentException(merge + " conflicts with " + this +
+ ": different transforms");
+
+ if (!merge.getDataType().equals(getDataType()))
+ throw new IllegalArgumentException(merge + " conflicts with " + this +
+ ": different types");
+
+ if (!merge.isImplicit())
+ setImplicit(false);
+
+ if (isHeadOf(this.sourceIterator(),merge.sourceIterator())) {
+ // Ok
+ }
+ else if (isHeadOf(merge.sourceIterator(),this.sourceIterator())) {
+ sources=new LinkedHashSet<>(merge.sources);
+ }
+ else {
+ throw new IllegalArgumentException(merge + " conflicts with " + this +
+ ": on source list must be the start of the other");
+ }
+
+ destinations.addAll(merge.destinations);
+
+ return this;
+ }
+
+ public boolean hasSource(String name) {
+ for (Source s : sources) {
+ if (s.getName().equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the second list is the start of the first list
+ */
+ private boolean isHeadOf(Iterator<?> full, Iterator<?> head) {
+ while (head.hasNext()) {
+ if (!full.hasNext()) return false;
+
+ if (!full.next().equals(head.next())) return false;
+ }
+ return true;
+ }
+
+ private String getDestinationString()
+ {
+ StringBuilder destinationString = new StringBuilder("destinations(");
+ for (String destination : destinations) {
+ destinationString.append(destination).append(" ");
+ }
+ destinationString.append(")");
+ return destinationString.toString();
+ }
+
+ public String toString() {
+ return
+ "summary field '" + getName() + ' ' + getDestinationString() +
+ "' [type: '" + getDataType().getName() +
+ "' transform: '" + transform +
+ "', source: '" + toString(sources) +
+ "', to '" + toString(destinations) + "']";
+ }
+
+ /** returns a string which aids locating this field in the source search definition */
+ public String toLocateString() {
+ return "'summary " + getName() + " type " + toLowerCase(getDataType().getName()) + "' in '" + getDestinationString() + "'";
+ }
+
+ public SummaryField clone() {
+ try {
+ SummaryField clone=(SummaryField)super.clone();
+ if (this.sources!=null)
+ clone.sources=new LinkedHashSet<>(this.sources);
+ if (this.destinations!=null)
+ clone.destinations=new LinkedHashSet<>(destinations);
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Programming error");
+ }
+ }
+
+ public VsmCommand getVsmCommand() {
+ return vsmCommand;
+ }
+
+ public void setVsmCommand(VsmCommand vsmCommand) {
+ this.vsmCommand = vsmCommand;
+ }
+
+ /** Adds a property to this summary field */
+ public void addProperty(String name, String value) {
+ properties.add(new Property(name, value));
+ }
+
+ public List<Property> getProperties() {
+ return properties;
+ }
+
+ /**
+ * The command used when using data from this SummaryField to generate StreamingSummary config (vsmsummary).
+ * Not used for ordinary Summary config.
+ * @author vegardh
+ *
+ */
+ public enum VsmCommand {
+ NONE("NONE"),
+ FLATTENSPACE("FLATTENSPACE"),
+ FLATTENJUNIPER("FLATTENJUNIPER");
+
+ private String cmd="";
+ private VsmCommand(String cmd) {
+ this.cmd=cmd;
+ }
+ @Override
+ public String toString() {
+ return cmd;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java
new file mode 100644
index 00000000000..05092d50951
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java
@@ -0,0 +1,99 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+/**
+ * A value class representing a search time
+ * transformation on a summary field.
+ *
+ * @author bratseth
+ */
+public enum SummaryTransform {
+
+ NONE("none"),
+ ATTRIBUTE("attribute"),
+ BOLDED("bolded"),
+ DISTANCE("distance"),
+ DYNAMICBOLDED("dynamicbolded"),
+ DYNAMICTEASER("dynamicteaser"),
+ POSITIONS("positions"),
+ RANKFEATURES("rankfeatures"),
+ SUMMARYFEATURES("summaryfeatures"),
+ TEXTEXTRACTOR("textextractor"),
+ GEOPOS("geopos");
+
+ private String name;
+
+ private SummaryTransform(String name) {
+ this.name=name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /** Returns the bolded version of this transform if possible, throws if not */
+ public SummaryTransform bold() {
+ switch (this) {
+ case NONE:
+ case BOLDED:
+ return BOLDED;
+
+ case DYNAMICBOLDED:
+ case DYNAMICTEASER:
+ return DYNAMICBOLDED;
+
+ default:
+ throw new IllegalArgumentException("Can not bold a '" + this + "' field.");
+ }
+ }
+
+ /** Returns the unbolded version of this transform */
+ public SummaryTransform unbold() {
+ switch (this) {
+ case NONE:
+ case BOLDED:
+ return NONE;
+
+ case DYNAMICBOLDED:
+ return DYNAMICTEASER;
+
+ default:
+ return this;
+ }
+ }
+
+ /** Returns whether this value is bolded */
+ public boolean isBolded() {
+ return this==BOLDED || this==DYNAMICBOLDED;
+ }
+
+ /** Whether this is dynamically generated, both teasers and bolded fields are dynamic */
+ public boolean isDynamic() {
+ return this==BOLDED || this==DYNAMICBOLDED || this==DYNAMICTEASER;
+ }
+
+ /** Returns whether this is a teaser, not the complete field value */
+ public boolean isTeaser() {
+ return this==DYNAMICBOLDED || this==DYNAMICTEASER;
+ }
+
+ /** Returns whether this transform always gets its value by accessing memory only */
+ public boolean isInMemory() {
+ switch (this) {
+ case ATTRIBUTE:
+ case DISTANCE:
+ case POSITIONS:
+ case GEOPOS:
+ case RANKFEATURES:
+ case SUMMARYFEATURES:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ public String toString() {
+ return name;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/AbstractService.java b/config-model/src/main/java/com/yahoo/vespa/model/AbstractService.java
new file mode 100644
index 00000000000..13d85c3a955
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/AbstractService.java
@@ -0,0 +1,516 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.PortInfo;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.filedistribution.PathDoesNotExistException;
+
+import java.util.*;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * Superclass for all Processes.
+ *
+ * @author gjoranv
+ */
+public abstract class AbstractService extends AbstractConfigProducer<AbstractConfigProducer<?>> implements Service {
+
+ private static final long serialVersionUID = 1L;
+
+ // The physical host this Service runs on.
+ private HostResource hostResource = null;
+
+ /**
+ * Identifier that ensures that multiple instances of the same
+ * Service subclass will have unique names on the host. The first
+ * instance of one kind of Service will have the id 1, and the id
+ * will increase by 1 for each new instance.
+ * TODO: Do something more intelligent in Host?
+ */
+ private int id = 0;
+
+ /** The actual base port for this Service. */
+ private int basePort;
+
+ /** The ports allocated to this Service. */
+ private List<Integer> ports = new ArrayList<>();
+
+ /** The optional JVM execution args for this Service. */
+ // Please keep non-null, as passed to command line in service startup
+ private String jvmArgs = "";
+
+ /** The optional PRELOAD libraries for this Service. */
+ // Please keep non-null, as passed to command line in service startup
+ private String preload = Defaults.getDefaults().vespaHome() + "lib64/vespa/malloc/libvespamalloc.so";
+
+ // If larger or equal to 0 it mean that explicit mmaps shall not be included in coredump.
+ private long mmapNoCoreLimit = -1l;
+
+ /** The ports metainfo object */
+ protected PortsMeta portsMeta = new PortsMeta();
+
+ /**
+ * Custom properties that a service may populate to communicate
+ * more key/value pairs to the service-list dump.
+ * Supported key datatypes are String, and values may be String or Integer.
+ */
+ private HashMap<String, Object> serviceProperties = new LinkedHashMap<>();
+
+ /** The affinity properties of this service. */
+ private Optional<Affinity> affinity = Optional.empty();
+
+ private boolean initialized = false;
+
+ /**
+ * Preferred constructor when building from XML. Use this if you are building
+ * in doBuild() in an AbstractConfigProducerBuilder.
+ * build() will call initService() in that case, after setting hostalias and baseport.
+ * @param parent Parent config producer in the model tree.
+ * @param name Name of this service.
+ */
+ public AbstractService(AbstractConfigProducer parent, String name) {
+ super(parent, name);
+ }
+
+ /**
+ * Only used for testing. Stay away.
+ * @param name Name of this service.
+ */
+ public AbstractService(String name) {
+ super(name);
+ }
+
+ /**
+ * Distribute affinity on a collection of services. Services that are located on the same host
+ * will be assigned a specific cpu socket on that host.
+ *
+ * @param services A {@link Collection} of services of the same type, not necessarily on the same host.
+ */
+ public static <SERVICE extends AbstractService> void distributeCpuSocketAffinity(Collection<SERVICE> services) {
+ Map<HostResource, List<SERVICE>> affinityMap = new HashMap<>();
+ for (SERVICE service : services) {
+ if (!affinityMap.containsKey(service.getHostResource())) {
+ affinityMap.put(service.getHostResource(), new ArrayList<SERVICE>());
+ }
+ int cpuSocket = affinityMap.get(service.getHostResource()).size();
+ affinityMap.get(service.getHostResource()).add(service);
+ service.setAffinity(new Affinity.Builder().cpuSocket(cpuSocket).build());
+ }
+ }
+
+ /**
+ * Helper method to avoid replicating code.
+ *
+ * @param hostResource The physical host on which this service should run.
+ * @param userPort The wanted port given by the user.
+ */
+ private void initService(HostResource hostResource, int userPort) {
+ if (initialized) {
+ throw new IllegalStateException("Service '" + getConfigId() + "' already initialized.");
+ }
+ if (hostResource == null) {
+ throw new RuntimeException("No host found for service '" + getServiceName() + "'. " +
+ "The hostalias is probably missing from hosts.xml.");
+ }
+ id = getIndex(hostResource);
+ ports = hostResource.allocateService(this, getInstanceWantedPort(userPort));
+ initialized = true;
+ }
+
+ /**
+ * Called by builder class which has not given the host or port in a constructor, hence
+ * initService is not yet run for this.
+ */
+ public void initService() {
+ initService(this.hostResource, this.basePort);
+ }
+
+ /**
+ * Returns the desired base port for the first instance of the
+ * service type. Returns '0' as default, which means that the
+ * service type should use the default port allocation mechanism.
+ *
+ * @return The desired base port for the first instance of the service type.
+ */
+ public int getWantedPort() {
+ return 0;
+ }
+
+ /**
+ * Returns the desired base port for this service instance, '0' if
+ * it should use the default port allocation mechanism.
+ *
+ * @param userWantedPort The wanted port given by the user.
+ * @return The desired base port for this service instance, '0' by default
+ */
+ private int getInstanceWantedPort(int userWantedPort) {
+ int wantedPort = 0;
+ if (userWantedPort == 0) {
+ if (requiresWantedPort())
+ wantedPort = getWantedPort();
+ else if (getWantedPort() > 0)
+ wantedPort = getWantedPort() + ((getId() - 1) * getPortCount());
+ } else {
+ // User defined from spec
+ wantedPort = userWantedPort;
+/* if ((wantedPort >= Host.BASE_PORT) &&
+ (wantedPort <= (Host.BASE_PORT + Host.MAX_PORTS))) {
+ throw new RuntimeException
+ ("Attribute 'basePort=" + wantedPort +
+ "' is not allowed to be inside Vespa's reserved port range "
+ + Host.BASE_PORT + "-"
+ + (Host.BASE_PORT + Host.MAX_PORTS) + ".");
+ }
+*/
+ }
+ return wantedPort;
+ }
+
+ /**
+ * Override if the desired base port (returned by getWantedPort()) is the only allowed base port.
+ *
+ * @return false by default
+ */
+ public boolean requiresWantedPort() {
+ return false;
+ }
+
+ /**
+ * Override if the services does not require consecutive port numbers. I.e. if any ports
+ * in addition to the baseport should be allocated from Vespa's default port range.
+ *
+ * @return true by default
+ */
+ public boolean requiresConsecutivePorts() {
+ return true;
+ }
+
+ /**
+ * Gets the ports metainfo object. The service implementation
+ * must populate this object in the constructor.
+ */
+ public PortsMeta getPortsMeta() {
+ return portsMeta;
+ }
+
+ /**
+ * Computes and returns the i'th port for this service, based on
+ * this Service's baseport.
+ *
+ * @param i The offset from 'basePort' of the port to return
+ * @return the i'th port relative to the base port.
+ * @throws IllegalStateException if i is out of range.
+ */
+ public int getRelativePort(int i) {
+ if (ports.size() < 1) {
+ throw new IllegalStateException
+ ("Requested port with offset " + i + " for service that " +
+ "has not reserved any ports: " + this);
+ }
+ if (i >= ports.size()) {
+ throw new IllegalStateException
+ ("Requested port with offset " + i + " for service that " +
+ "only has reserved " + ports.size() + " ports: " + this);
+ }
+ return ports.get(i);
+ }
+
+ /**
+ * Must be overridden by services that should be started by
+ * config-sentinel. The returned value will be used in
+ * config-sentinel configuration. Returns null by default.
+ *
+ * @return null by default.
+ */
+ public String getStartupCommand() {
+ return null;
+ }
+
+ public Optional<String> getPreShutdownCommand() {
+ return Optional.empty();
+ }
+
+ /**
+ * Tells if this service should be autostarted by
+ * config-sentinel. Returned value will be used to configure the
+ * config-sentinel.
+ *
+ * @return true by default.
+ */
+ public boolean getAutostartFlag() {
+ return true;
+ }
+
+ /**
+ * Tells if this service should be autorestarted by
+ * config-sentinel. Returned value will be used to configure the
+ * config-sentinel.
+ *
+ * @return true by default.
+ */
+ public boolean getAutorestartFlag() {
+ return true;
+ }
+
+ /**
+ * Returns the name that identifies this service for the config-sentinel.
+ *
+ * @return the name that identifies this service for the config-sentinel.
+ */
+ public String getServiceName() {
+ return getServiceType() + ((id == 1) ? "" : Integer.toString(id));
+ }
+
+ /**
+ * Returns the type of service. This is the class name without the
+ * package prefix by default.
+ */
+ public String getServiceType() {
+ return toLowerCase(getShortClassName());
+ }
+
+ /**
+ * Strips the package prefix and returns the short classname.
+ *
+ * @return classname without package prefix.
+ */
+ private String getShortClassName() {
+ Class myClass = getClass();
+ Package myPackage = myClass.getPackage();
+ return myClass.getName().substring(1 + myPackage.getName().length());
+ }
+
+ /**
+ * @return the physical host on which this service runs.
+ */
+ public Host getHost() {
+ if (hostResource != null) {
+ return hostResource.getHost();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return The hostname on which this service runs.
+ */
+ public String getHostName() {
+ return hostResource.getHostName();
+ }
+
+ /**
+ * @return The id (index) of this service on the host where it runs
+ */
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Computes a number that identifies the service on the given
+ * host. The number of services of the same type (Class) is
+ * counted and the number is returned.
+ *
+ * @param host the host on which the service will run
+ * @return id number for the given service.
+ */
+ // TODO: Do something more intelligent in the Host class..?
+ protected int getIndex(HostResource host) {
+ int i = 0;
+ for (Service s : host.getServices()) {
+ //if (s.getClass().equals(getClass()) && (s != this)) {
+ if (s.getServiceType().equals(getServiceType()) && (s != this)) {
+ i++;
+ }
+ }
+ return i + 1;
+ }
+
+ @Override
+ public ServiceInfo getServiceInfo() {
+ Set<PortInfo> portInfos = new LinkedHashSet<>();
+ for (int i = 0; i < portsMeta.getNumPorts(); i++) {
+ portInfos.add(new PortInfo(ports.get(i), new LinkedHashSet<>(portsMeta.getTagsAt(i))));
+ }
+ Map<String, String> properties = new LinkedHashMap<>();
+ for (Map.Entry<String, Object> prop : serviceProperties.entrySet()) {
+ properties.put(prop.getKey(), prop.getValue().toString());
+ }
+ return new ServiceInfo(getServiceName(), getServiceType(), portInfos, properties, getConfigId(), getHostName());
+ }
+
+ /**
+ * Sets a service property value for the given key.
+ *
+ * @param key a key used for this property
+ * @param value a String value associated with the key
+ * @return this service
+ */
+ public AbstractService setProp(String key, String value) {
+ serviceProperties.put(key, value);
+ return this;
+ }
+
+ /**
+ * Sets a service property value for the given key.
+ *
+ * @param key a key used for this property
+ * @param value an Integer value associated with the key
+ * @return this service
+ */
+ public AbstractService setProp(String key, Integer value) {
+ serviceProperties.put(key, value);
+ return this;
+ }
+
+ /**
+ * Gets a service property value mapped to the given key
+ * as a String, or null if no such key exists.
+ *
+ * @param key a key used for lookup in the service properties
+ * @return the associated String value for the given key, or null
+ */
+ public String getServicePropertyString(String key) {
+ return getServicePropertyString(key, null);
+ }
+
+ public String getServicePropertyString(String key, String defStr) {
+ Object result = serviceProperties.get(key);
+ return (result == null) ? defStr : result.toString();
+ }
+
+ /** Optional execution args for this service */
+ public String getJvmArgs() {
+ return jvmArgs;
+ }
+ public void setJvmArgs(String args) {
+ jvmArgs = (args == null) ? "" : args;
+ }
+ public void appendJvmArgs(String args) {
+ if ((args != null) && ! "".equals(args)) {
+ setJvmArgs(jvmArgs + getSeparator(jvmArgs) + args);
+ }
+ }
+ private static String getSeparator(String current) {
+ return ("".equals(current)) ? "" : " ";
+ }
+ public void prependJvmArgs(String args) {
+ if ((args != null) && ! "".equals(args)) {
+ setJvmArgs(args + getSeparator(jvmArgs) + jvmArgs);
+ }
+ }
+ public String getPreLoad() { return preload; }
+ public void setPreLoad(String preload) {
+ this.preload = preload;
+ }
+ public long getMMapNoCoreLimit() { return mmapNoCoreLimit; }
+ public void setMMapNoCoreLimit(long noCoreLimit) {
+ this.mmapNoCoreLimit = noCoreLimit;
+ }
+
+ public String getMMapNoCoreEnvVariable() {
+ return (getMMapNoCoreLimit() >= 0l)
+ ? "VESPA_MMAP_NOCORE_LIMIT=" + getMMapNoCoreLimit() + " "
+ : "";
+ }
+
+ /**
+ * WARNING: should only be called before initService(), otherwise call at own risk!
+ */
+ public void setBasePort(int wantedPort) {
+ if (initialized && getPortCount() > 0) {
+ // This means initServices has been called already, so you are being nasty, trying to change ports!
+ // Try to allocate the new ports if they are available.
+ if (!hostResource.isPortRangeAvailable(wantedPort, 1) ||
+ (requiresConsecutivePorts() && ! hostResource.isPortRangeAvailable(wantedPort, getPortCount())))
+ throw new IllegalStateException("For service with id " + getConfigId() + ": setBasePort is called after initService, and ports are not available.");
+
+ hostResource.deallocatePorts(this);
+ ports = hostResource.allocatePorts(this, getInstanceWantedPort(wantedPort));
+ }
+
+ this.basePort = wantedPort;
+ }
+
+ /** Temporary hack: reserve port with index 0
+ * Must be done this way since the system test framework
+ * currently uses the first port as container http port.
+ */
+ public void reservePortPrepended(int port) {
+ hostResource.reservePort(this, port);
+ ports.add(0, port);
+ }
+
+ public void setHostResource(HostResource hostResource) {
+ this.hostResource = hostResource;
+ }
+
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ /**
+ * Add the given file to the application's file distributor.
+ *
+ * @param relativePath path to the file, relative to the app package.
+ * @return the file reference hash
+ */
+ public FileReference sendFile(String relativePath) {
+ try {
+ return getRoot().getFileDistributor().sendFileToHost(relativePath, getHost());
+ } catch (PathDoesNotExistException e) {
+ throw new RuntimeException("File does not exist: '" + relativePath + "'.");
+ }
+ }
+
+ /**
+ * Sets up this service to be included when generating monitoring config.
+ * The ymon service name used will be {@link #getServiceType()}
+ */
+ public void monitorService() {
+ monitorService(getServiceType());
+ }
+
+ /**
+ * Sets up this service to be included when generating ymon config.
+ * @param ymonServiceName the ymon service name to be used
+ */
+ public void monitorService(String ymonServiceName) {
+ setProp("ymonService", ymonServiceName);
+ }
+
+ /**
+ *
+ * The service HTTP port for health status
+ * @return portnumber
+ */
+ public int getHealthPort() {return -1;}
+
+ /**
+ * Overridden by subclasses. List of default dimensions to be added to this services metrics
+ * @return The default dimensions for this service
+ */
+ public HashMap<String, String> getDefaultMetricDimensions(){
+ return new LinkedHashMap<>();
+ }
+
+ // For testing
+ public int getNumPortsAllocated() {
+ return ports.size();
+ }
+
+ public HostResource getHostResource() {
+ return hostResource;
+ }
+
+ public Optional<Affinity> getAffinity() {
+ return affinity;
+ }
+
+ public void setAffinity(Affinity affinity) {
+ this.affinity = Optional.ofNullable(affinity);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/Affinity.java b/config-model/src/main/java/com/yahoo/vespa/model/Affinity.java
new file mode 100644
index 00000000000..d621c299a5d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/Affinity.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+/**
+ * Represents a set of affinities that can be expressed by a service. Currently only supports
+ * CPU socket affinity.
+ *
+ * @author lulf
+ * @since 5.12
+ */
+public class Affinity {
+ private final int cpuSocket;
+
+ private Affinity(int cpuSocket) {
+ this.cpuSocket = cpuSocket;
+ }
+
+ public int cpuSocket() {
+ return cpuSocket;
+ }
+
+ public static Affinity none() {
+ return new Builder().build();
+ }
+
+ public static class Builder {
+ private int cpuSocket = -1;
+ public Builder cpuSocket(int cpuSocket) {
+ this.cpuSocket = cpuSocket;
+ return this;
+ }
+
+ public Affinity build() {
+ return new Affinity(cpuSocket);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/Client.java b/config-model/src/main/java/com/yahoo/vespa/model/Client.java
new file mode 100644
index 00000000000..15685f5f669
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/Client.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * This is a placeholder config producer that makes global configuration available through a single identifier. This
+ * is added directly to the {@link ApplicationConfigProducerRoot} producer, and so can be accessed by the simple "client" identifier.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class Client extends AbstractConfigProducer {
+
+ /**
+ * Constructs a client config producer that is added as a child to
+ * the given config producer.
+ *
+ * @param parent The parent config producer.
+ */
+ public Client(AbstractConfigProducer parent) {
+ super(parent, "client");
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/ConfigProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/ConfigProducer.java
new file mode 100644
index 00000000000..852e4e73331
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/ConfigProducer.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Map;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigInstance.Builder;
+import com.yahoo.config.model.producer.UserConfigRepo;
+
+/**
+ * Interface that should be implemented by all config producing modules
+ * in the vespa model.
+ *
+ * @author gjoranv
+ */
+public interface ConfigProducer extends com.yahoo.config.ConfigInstance.Producer {
+
+ /**
+ * @return the configId of this ConfigProducer.
+ */
+ public String getConfigId();
+
+ /**
+ * @return The one and only HostSystem of the root node
+ */
+ public HostSystem getHostSystem();
+
+ /** Returns the user configs of this */
+ public UserConfigRepo getUserConfigs();
+
+ /**
+ * @return this ConfigProducer's children (only 1st level)
+ */
+ public Map<String,? extends ConfigProducer> getChildren();
+
+ /**
+ * @return a List of all Services that are descendants to this ConfigProducer
+ */
+ public List<Service> getDescendantServices();
+
+ /**
+ * Writes files that need to be written. The files will usually
+ * only be written when the Vespa model is generated through the
+ * deploy-application script.
+ * gv: This is primarily intended for debugging.
+ * @param directory directory to write files to
+ * @throws java.io.IOException if writing fails
+ */
+ public void writeFiles(File directory) throws IOException;
+
+ /**
+ * Dump the three of config producers to the specified stream.
+ * @param out The stream to print to, e.g. System.out
+ */
+ public void dump(PrintStream out);
+
+ /**
+ * Build config from this and all parent ConfigProducers,
+ * such that the root node's config will be added first, and this
+ * ConfigProducer's config last in the returned builder.
+ *
+ * @param builder The builder implemented by the concrete ConfigInstance class
+ * @return true if a model config producer was found, so config was applied
+ */
+ boolean cascadeConfig(Builder builder);
+
+ /**
+ * Adds user config override from this ConfigProducer to the existing builder
+ *
+ * @param builder The ConfigBuilder to add user config overrides.
+ * @return true if overrides were added, false if not.
+ */
+ public boolean addUserConfig(ConfigInstance.Builder builder);
+
+ /**
+ * check constraints depending on the state of the vespamodel graph.
+ * When overriding, you must invoke super.
+ */
+ public void validate() throws Exception;
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/ConfigProducerRoot.java b/config-model/src/main/java/com/yahoo/vespa/model/ConfigProducerRoot.java
new file mode 100644
index 00000000000..d0bb89beca7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/ConfigProducerRoot.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.filedistribution.FileDistributor;
+
+import java.util.Set;
+
+
+/**
+ * Intended to be used as an external interface to the vespa model root.
+ *
+ * @author tonytv
+ */
+public interface ConfigProducerRoot extends ConfigProducer {
+
+ /**
+ * Adds the given producer (at any depth level) as descendant to this root nodes.
+ *
+ * @param id string id of descendant
+ * @param descendant the producer to add to this root node
+ */
+ void addDescendant(String id, AbstractConfigProducer descendant);
+
+ /**
+ * @return an unmodifiable copy of the set of configIds in this root.
+ */
+ Set<String> getConfigIds();
+
+ ConfigInstance.Builder getConfig(ConfigInstance.Builder builder, String configId);
+
+ /**
+ * Resolves config of the given type and config id.
+ * @param clazz The type of config
+ * @param configId The config id
+ * @return A config instance of the given type
+ */
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> clazz, String configId);
+
+ /**
+ * Get the global deploy state of this model.
+ */
+ DeployState getDeployState();
+
+ FileDistributor getFileDistributor();
+
+ Admin getAdmin();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/ConfigProxy.java b/config-model/src/main/java/com/yahoo/vespa/model/ConfigProxy.java
new file mode 100644
index 00000000000..22fdca6af50
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/ConfigProxy.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+/**
+ * There is one config proxy running on each Vespa host, and one instance of
+ * this class is therefore created by each instance of class {@link
+ * com.yahoo.vespa.model.Host}.
+ *
+ * NOTE: The Config proxy is not started by the config system, and
+ * does not receive any config. It's included here so we know what host
+ * it runs on, and to give an error message if another service tries
+ * to reserve the port it is using.
+ *
+ * @author Vidar Larsen
+ * @author Harald Musum
+ */
+public class ConfigProxy extends AbstractService {
+
+ /**
+ * Creates a new ConfigProxy instance.
+ *
+ * @param host hostname
+ */
+ public ConfigProxy(Host host) {
+ super(host, "configproxy");
+ portsMeta.on(0).tag("rpc").tag("client").tag("status").tag("rpc").tag("admin");
+ setProp("clustertype", "hosts");
+ setProp("clustername", "admin");
+ }
+
+ /**
+ * Returns the desired base port for this service.
+ */
+ public int getWantedPort() { return 19090; }
+
+ /**
+ * The desired base port is the only allowed base port.
+ */
+ public boolean requiresWantedPort() { return true; }
+
+ /**
+ * ConfigProxy needs one rpc client port.
+ *
+ * @return The number of ports reserved by the config proxy
+ */
+ public int getPortCount() { return 1; }
+
+ /**
+ * The config proxy is not started by the config system!
+ */
+ public boolean getAutostartFlag() { return false; }
+
+ /**
+ * The config proxy is not started by the config system!
+ */
+ public boolean getAutorestartFlag() { return false; }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/ConfigSentinel.java b/config-model/src/main/java/com/yahoo/vespa/model/ConfigSentinel.java
new file mode 100644
index 00000000000..adcb93bc539
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/ConfigSentinel.java
@@ -0,0 +1,107 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Zone;
+
+/**
+ * There is one config-sentinel running on each Vespa host, and one
+ * instance of this class is therefore created by each instance of
+ * class {@link Host}.
+ *
+ * @author gjoranv
+ */
+public class ConfigSentinel extends AbstractService implements SentinelConfig.Producer {
+
+ private final ApplicationId applicationId;
+ private final Zone zone;
+
+ /**
+ * Constructs a new ConfigSentinel for the given host.
+ *
+ * @param host Physical host on which to run.
+ */
+ public ConfigSentinel(Host host, ApplicationId applicationId, Zone zone) {
+ super(host, "sentinel");
+ this.applicationId = applicationId;
+ this.zone = zone;
+ portsMeta.on(0).tag("rpc").tag("notyet");
+ portsMeta.on(1).tag("telnet").tag("interactive").tag("http").tag("state");
+ setProp("clustertype", "hosts");
+ setProp("clustername", "admin");
+ }
+
+ /**
+ * Returns the desired base port for this service.
+ */
+ public int getWantedPort() { return 19097; }
+
+ /**
+ * The desired base port is the only allowed base port.
+ */
+ public boolean requiresWantedPort() { return true; }
+
+ /**
+ * @return The number of ports reserved by the Sentinel.
+ */
+ public int getPortCount() { return 2; }
+
+ @Override
+ public int getHealthPort() {return getRelativePort(1); }
+
+ /**
+ * Overrides parent method as this is named config-sentinel and not configsentinel all over Vespa
+ * @return service type for config-sentinel
+ */
+ public String getServiceType(){
+ return "config-sentinel";
+ }
+
+ @Override
+ public void getConfig(SentinelConfig.Builder builder) {
+ builder.application(getApplicationConfig());
+ for (Service s : getHostResource().getServices()) {
+ if (s.getStartupCommand() != null) {
+ builder.service(getServiceConfig(s));
+ }
+ }
+ }
+
+ private SentinelConfig.Application.Builder getApplicationConfig() {
+ SentinelConfig.Application.Builder builder = new SentinelConfig.Application.Builder();
+ builder.tenant(applicationId.tenant().value());
+ builder.name(applicationId.application().value());
+ builder.environment(zone.environment().value());
+ builder.region(zone.region().value());
+ builder.instance(applicationId.instance().value());
+ return builder;
+ }
+
+ private SentinelConfig.Service.Builder getServiceConfig(Service s) {
+ SentinelConfig.Service.Builder serviceBuilder = new SentinelConfig.Service.Builder();
+ serviceBuilder.command(s.getStartupCommand());
+ serviceBuilder.name(s.getServiceName());
+ serviceBuilder.autostart(s.getAutostartFlag());
+ serviceBuilder.autorestart(s.getAutorestartFlag());
+ serviceBuilder.id(s.getConfigId());
+ serviceBuilder.affinity(getServiceAffinity(s));
+ setPreShutdownCommand(serviceBuilder, s);
+ return serviceBuilder;
+ }
+
+ private void setPreShutdownCommand(SentinelConfig.Service.Builder serviceBuilder, Service service) {
+ if (service.getPreShutdownCommand().isPresent()) {
+ serviceBuilder.preShutdownCommand(service.getPreShutdownCommand().get());
+ }
+ }
+
+
+ private SentinelConfig.Service.Affinity.Builder getServiceAffinity(Service s) {
+ SentinelConfig.Service.Affinity.Builder builder = new SentinelConfig.Service.Affinity.Builder();
+ if (s.getAffinity().isPresent()) {
+ builder.cpuSocket(s.getAffinity().get().cpuSocket());
+ }
+ return builder;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/Host.java b/config-model/src/main/java/com/yahoo/vespa/model/Host.java
new file mode 100644
index 00000000000..99109a881a1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/Host.java
@@ -0,0 +1,121 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.util.*;
+import java.util.logging.Level;
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * A physical host, running a set of services.
+ * The identity of a host is its hostname. Hosts are comparable on their host name.
+ *
+ * @author gjoranv
+ */
+public final class Host extends AbstractConfigProducer<AbstractConfigProducer<?>> implements SentinelConfig.Producer, Comparable<Host> {
+
+ private ConfigSentinel configSentinel = null;
+ private final String hostname;
+ private final boolean multitenant;
+
+ /**
+ * Constructs a new Host instance.
+ *
+ * @param parent parent AbstractConfigProducer in the config model.
+ * @param hostname hostname for this host.
+ */
+ public Host(AbstractConfigProducer parent, String hostname) {
+ this(parent, hostname, false);
+ }
+
+ private Host(AbstractConfigProducer parent, String hostname, boolean multitenant) {
+ super(parent, hostname);
+ Objects.requireNonNull(hostname, "The host name of a host cannot be null");
+ this.multitenant = multitenant;
+ this.hostname = hostname;
+ if (parent instanceof HostSystem) {
+ checkName((HostSystem) parent, hostname);
+ }
+ }
+
+ private void checkName(HostSystem parent, String hostname) {
+ // Give a warning if the host does not exist
+ if (! parent.getIp(hostname).equals("0.0.0.0")) {
+ // Host exists - warn if given hostname is not a fully qualified one.
+ String canonical=hostname;
+ try {
+ canonical = parent.getCanonicalHostname(hostname);
+ } catch (UnknownHostException e) {
+ deployLogger().log(Level.WARNING, "Unable to find canonical hostname of host: " + hostname);
+ }
+ if ((null != canonical) && (! hostname.equals(canonical))) {
+ deployLogger().log(Level.WARNING, "Host named '" + hostname + "' will not receive any config " +
+ "since it does not match its canonical hostname: " + canonical);
+ }
+ }
+ }
+
+ public static Host createMultitenantHost(AbstractConfigProducer parent, String hostname) {
+ return new Host(parent, hostname, true);
+ }
+
+ // For testing
+ Host(AbstractConfigProducer parent) {
+ super(parent, "testhost");
+ hostname = "testhost";
+ configSentinel = null;
+ multitenant = false;
+ }
+
+ public String getHostName() {
+ return hostname;
+ }
+
+ public boolean isMultitenant() {
+ return multitenant;
+ }
+
+ /**
+ * Returns the string representation of this Host object.
+ * @return The string representation of this Host object.
+ */
+ public String toString() {
+ return "host '" + getHostName() + "'";
+ }
+
+ @Override
+ public void writeFiles(File directory) throws IOException {
+ }
+
+ @Override
+ public void getConfig(SentinelConfig.Builder builder) {
+ // TODO (MAJOR_RELEASE): This shouldn't really be here, but we need to make sure users can upgrade if we change sentinel to use hosts/<hostname>/sentinel instead of hosts/<hostname>
+ // as config id. We should probably wait for a major release
+ if (configSentinel != null) {
+ configSentinel.getConfig(builder);
+ }
+ }
+
+ public void setConfigSentinel(ConfigSentinel configSentinel) {
+ this.configSentinel = configSentinel;
+ }
+
+ @Override
+ public int hashCode() { return hostname.hashCode(); }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if ( ! (other instanceof Host)) return false;
+ return ((Host)other).hostname.equals(hostname);
+ }
+
+ @Override
+ public int compareTo(Host other) {
+ return this.hostname.compareTo(other.hostname);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/HostResource.java b/config-model/src/main/java/com/yahoo/vespa/model/HostResource.java
new file mode 100644
index 00000000000..782487ee12c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/HostResource.java
@@ -0,0 +1,251 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.*;
+import java.util.logging.Level;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+/**
+ * A host representation. The identity of this is the identity of its Host.
+ * TODO: Merge with {@link Host}
+ * Host resources are ordered by their host order.
+ *
+ * @author lulf
+ * @since 5.12
+ */
+public class HostResource implements Comparable<HostResource> {
+
+ public final static int BASE_PORT = 19100;
+ public final static int MAX_PORTS = 799;
+ private final Host host;
+
+ // Map from "sentinel name" to service
+ private final Map<String,Service> services = new LinkedHashMap<>();
+ private final Map<Integer, Service> portDB = new LinkedHashMap<>();
+
+ private int allocatedPorts = 0;
+
+ /**
+ * Create a new {@link HostResource} bound to a specific {@link com.yahoo.vespa.model.Host}.
+ *
+ * @param host {@link com.yahoo.vespa.model.Host} object to bind to.
+ */
+ public HostResource(Host host) {
+ this.host = host;
+ }
+
+ /**
+ * Return the currently bounded {@link com.yahoo.vespa.model.Host}.
+ * @return the {@link com.yahoo.vespa.model.Host} if bound, null if not.
+ */
+ public Host getHost() { return host; }
+
+ /**
+ * Returns the baseport of the first available port range of length numPorts,
+ * or 0 if there is no range of that length available.
+ *
+ * @param numPorts The length of the desired port range.
+ * @return The baseport of the first available range, or 0 if no range is available.
+ */
+ public int nextAvailableBaseport(int numPorts) {
+ int range = 0;
+ int port = BASE_PORT;
+ for (; port < BASE_PORT + MAX_PORTS && (range < numPorts); port++) {
+ if (portDB.containsKey(port)) {
+ range = 0;
+ continue;
+ }
+ range++;
+ }
+ return range == numPorts ?
+ port - range :
+ 0;
+ }
+
+ boolean isPortRangeAvailable(int start, int numPorts) {
+ int range = 0;
+ int port = start;
+ for (; port < BASE_PORT + MAX_PORTS && (range < numPorts); port++) {
+ if (portDB.containsKey(port)) {
+ return false;
+ }
+ range++;
+ }
+ return range == numPorts;
+ }
+
+ /**
+ * Adds service and allocates resources for it.
+ *
+ * @param service The Service to allocate resources for
+ * @param wantedPort the wanted port for this service
+ * @return The allocated ports for the Service.
+ */
+ List<Integer> allocateService(AbstractService service, int wantedPort) {
+ List<Integer> ports = allocatePorts(service, wantedPort);
+ assert (getService(service.getServiceName()) == null) :
+ ("There is already a service with name '" + service.getServiceName() + "' registered on " + this +
+ ". Most likely a programming error - all service classes must have unique names, even in different packages!");
+
+ services.put(service.getServiceName(), service);
+ return ports;
+ }
+
+ // TODO: make private when LocalApplication and all model services has stopped reallocating ports _after_ allocateServices has been called
+ List<Integer> allocatePorts(AbstractService service, int wantedPort) {
+ List<Integer> ports = new ArrayList<>();
+ if (service.getPortCount() < 1)
+ return ports;
+
+ int serviceBasePort = BASE_PORT + allocatedPorts;
+ if (wantedPort > 0) {
+ if (service.getPortCount() < 1) {
+ throw new RuntimeException(service + " wants baseport " + wantedPort +
+ ", but it has not reserved any ports, so it cannot name a desired baseport.");
+ }
+ if (service.requiresWantedPort() || canUseWantedPort(service, wantedPort, serviceBasePort))
+ serviceBasePort = wantedPort;
+ }
+
+ reservePort(service, serviceBasePort);
+ ports.add(serviceBasePort);
+
+ int remainingPortsStart = service.requiresConsecutivePorts() ?
+ serviceBasePort + 1:
+ BASE_PORT + allocatedPorts;
+ for (int i = 0; i < service.getPortCount() - 1; i++) {
+ int port = remainingPortsStart + i;
+ reservePort(service, port);
+ ports.add(port);
+ }
+ return ports;
+ }
+
+ // TODO: this is a hack to allow calling AbstractService.setBasePort _after_ the services has been initialized,
+ // i.e. modifying the baseport. Done by e.g. LocalApplication. Try to remove usage of this method!
+ void deallocatePorts(AbstractService service) {
+ for (Iterator<Map.Entry<Integer,Service>> i=portDB.entrySet().iterator(); i.hasNext();) {
+ Map.Entry<Integer, Service> e = i.next();
+ Service s = e.getValue();
+ if (s.equals(service))
+ i.remove();
+ }
+ }
+
+ private boolean canUseWantedPort(AbstractService service, int wantedPort, int serviceBasePort) {
+ for (int i = 0; i < service.getPortCount(); i++) {
+ int port = wantedPort + i;
+ if (portDB.containsKey(port)) {
+ AbstractService s = (AbstractService)portDB.get(port);
+ s.getRoot().getDeployState().getDeployLogger().log(Level.WARNING, service.getServiceName() +" cannot reserve port " + port + " on " +
+ this + ": Already reserved for " + s.getServiceName() +
+ ". Using default port range from " + serviceBasePort);
+ return false;
+ }
+ if (!service.requiresConsecutivePorts()) break;
+ }
+ return true;
+ }
+
+ /**
+ * Reserves the desired port for the given service, or throws as exception if the port
+ * is not available.
+ *
+ * @param service the service that wishes to reserve the port.
+ * @param port the port to be reserved.
+ */
+ void reservePort(AbstractService service, int port) {
+ if (portDB.containsKey(port)) {
+ portAlreadyReserved(service, port);
+ } else {
+ if (inVespasPortRange(port)) {
+ allocatedPorts++;
+ if (allocatedPorts > MAX_PORTS) {
+ noMoreAvailablePorts();
+ }
+ }
+ portDB.put(port, service);
+ }
+ }
+
+ private boolean inVespasPortRange(int port) {
+ return port >= BASE_PORT &&
+ port < BASE_PORT + MAX_PORTS;
+ }
+ private void portAlreadyReserved(AbstractService service, int port) {
+ AbstractService otherService = (AbstractService)portDB.get(port);
+ int nextAvailablePort = nextAvailableBaseport(service.getPortCount());
+ if (nextAvailablePort == 0) {
+ noMoreAvailablePorts();
+ }
+ String msg = (service.getClass().equals(otherService.getClass()) && service.requiresWantedPort())
+ ? "You must set port explicitly for all instances of this service type, except the first one. "
+ : "";
+ throw new RuntimeException(service.getServiceName() + " cannot reserve port " + port +
+ " on " + this + ": Already reserved for " + otherService.getServiceName() +
+ ". " + msg + "Next available port is: " + nextAvailablePort);
+ }
+
+
+ private void noMoreAvailablePorts() {
+ throw new RuntimeException
+ ("Too many ports are reserved in Vespa's port range (" +
+ BASE_PORT + ".." + (BASE_PORT+MAX_PORTS) + ") on " + this +
+ ". Move one or more services to another host, or outside this port range.");
+ }
+
+
+ /**
+ * Returns the service with the given "sentinel name" on this Host,
+ * or null if the name does not match any service.
+ *
+ * @param sentinelName the sentinel name of the service we want to return
+ * @return The service with the given sentinel name
+ */
+ public Service getService(String sentinelName) {
+ return services.get(sentinelName);
+ }
+
+ /**
+ * Returns a List of all services running on this Host.
+ * @return a List of all services running on this Host.
+ */
+ public List<Service> getServices() {
+ return new ArrayList<>(services.values());
+ }
+
+ public HostInfo getHostInfo() {
+ return new HostInfo(getHostName(), services.values().stream()
+ .map(service -> service.getServiceInfo())
+ .collect(Collectors.toSet()));
+ }
+
+ @Override
+ public String toString() {
+ return "host '" + host.getHostName() + "'";
+ }
+
+ public String getHostName() {
+ return host.getHostName();
+ }
+
+ @Override
+ public int hashCode() { return host.hashCode(); }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) return true;
+ if ( ! (other instanceof HostResource)) return false;
+ return ((HostResource)other).getHost().equals(this.getHost());
+ }
+
+ @Override
+ public int compareTo(HostResource other) {
+ return this.host.compareTo(other.host);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java b/config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java
new file mode 100644
index 00000000000..42feee08f43
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java
@@ -0,0 +1,203 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.config.provision.*;
+import com.yahoo.net.HostName;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+/**
+ * The parent node for all Host instances, and thus accessible
+ * to enable services to get their Host.
+ *
+ * @author gjoranv
+ */
+public class HostSystem extends AbstractConfigProducer<Host> {
+
+ private Map<String,String> ipAddresses = new LinkedHashMap<>();
+ private Map<String,String> hostnames = new LinkedHashMap<>();
+
+ private final Map<String, HostResource> hostname2host = new LinkedHashMap<>();
+ private final HostProvisioner provisioner;
+ private final Map<HostResource, Set<ClusterMembership>> mapping = new LinkedHashMap<>();
+
+ public HostSystem(AbstractConfigProducer parent, String name, HostProvisioner provisioner) {
+ super(parent, name);
+ this.provisioner = provisioner;
+ }
+
+ /**
+ * Returns the host with the given hostname.
+ *
+ * @param name the hostname of the host.
+ * @return the host with the given hostname.
+ */
+ public HostResource getHostByHostname(String name) {
+ // TODO: please eliminate the following ugly hack
+ if ("localhost.fortestingpurposesonly".equals(name)) {
+ String localhost = "localhost";
+ if ( ! getChildren().containsKey(localhost)) {
+ new Host(this, localhost);
+ }
+ return new HostResource(getChildren().get(localhost));
+ }
+ return hostname2host.get(name);
+ }
+
+ /**
+ * Returns the canonical name of a given host. This will cache names for faster lookup.
+ *
+ * @param hostname the hostname to retrieve the canonical hostname for.
+ * @return The canonical hostname, or null if unable to resolve.
+ * @throws UnknownHostException if the hostname cannot be resolved
+ */
+ public String getCanonicalHostname(String hostname) throws UnknownHostException {
+ if ( ! hostnames.containsKey(hostname)) {
+ hostnames.put(hostname, lookupCanonicalHostname(hostname));
+ }
+ return hostnames.get(hostname);
+ }
+
+ /**
+ * Static helper method that looks up the canonical name of a given host.
+ *
+ * @param hostname the hostname to retrieve the canonical hostname for.
+ * @return The canonical hostname, or null if unable to resolve.
+ * @throws UnknownHostException if the hostname cannot be resolved
+ */
+ public static String lookupCanonicalHostname(String hostname) throws UnknownHostException {
+ return java.net.InetAddress.getByName(hostname).getCanonicalHostName();
+ }
+
+ /**
+ * Returns the if address of a host.
+ *
+ * @param hostname the hostname to retrieve the ip address for.
+ * @return The string representation of the ip-address.
+ */
+ public String getIp(String hostname) {
+ if (ipAddresses.containsKey(hostname)) return ipAddresses.get(hostname);
+
+ String ipAddress;
+ if (hostname.startsWith(MockRoot.MOCKHOST)) {
+ ipAddress = "0.0.0.0";
+ } else {
+ try {
+ InetAddress address = InetAddress.getByName(hostname);
+ ipAddress = address.getHostAddress();
+ } catch (java.net.UnknownHostException e) {
+ log.warning("Unable to find valid IP address of host: " + hostname);
+ ipAddress = "0.0.0.0";
+ }
+ }
+ ipAddresses.put(hostname, ipAddress);
+ return ipAddress;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (HostResource host : mapping.keySet()) {
+ sb.append(host).append(",");
+ }
+ if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1);
+ return sb.toString();
+ }
+
+ public HostResource getHost(String hostAlias) {
+ HostSpec hostSpec = provisioner.allocateHost(hostAlias);
+ for (Map.Entry<HostResource, Set<ClusterMembership>> entrySet : mapping.entrySet()) {
+ HostResource resource = entrySet.getKey();
+ if (resource.getHostName().equals(hostSpec.hostname())) {
+ entrySet.getValue().add(hostSpec.membership().orElse(null));
+ return resource;
+ }
+ }
+ return addNewHost(hostSpec);
+ }
+
+ private HostResource addNewHost(HostSpec hostSpec) {
+ Host host = new Host(this, hostSpec.hostname());
+ HostResource hostResource = new HostResource(host);
+ hostname2host.put(host.getHostName(), hostResource);
+ Set<ClusterMembership> hostMemberships = new LinkedHashSet<>();
+ if (hostSpec.membership().isPresent())
+ hostMemberships.add(hostSpec.membership().get());
+ mapping.put(hostResource, hostMemberships);
+ return hostResource;
+ }
+
+ public List<HostResource> getHosts() {
+ return mapping.keySet().stream()
+ .filter(host -> !host.getHost().isMultitenant())
+ .collect(Collectors.toList());
+ }
+
+ public Map<HostResource, ClusterMembership> allocateHosts(ClusterSpec cluster, Capacity capacity, int groups, DeployLogger logger) {
+ List<HostSpec> allocatedHosts = provisioner.prepare(cluster, capacity, groups, new ProvisionDeployLogger(logger));
+ // TODO: Let hostresource own a membership rather than using a map?
+ Map<HostResource, ClusterMembership> retAllocatedHosts = new LinkedHashMap<>();
+ for (HostSpec host : allocatedHosts) {
+ // This is needed for single node host provisioner to work in unit tests for hosted vespa applications.
+ Optional<HostResource> existingHost = getExistingHost(host);
+ if (existingHost.isPresent()) {
+ retAllocatedHosts.put(existingHost.get(), host.membership().orElse(null));
+ } else {
+ retAllocatedHosts.put(addNewHost(host), host.membership().orElse(null));
+ }
+ }
+ return retAllocatedHosts;
+ }
+
+ private Optional<HostResource> getExistingHost(HostSpec key) {
+ List<HostResource> hosts = mapping.keySet().stream()
+ .filter(resource -> resource.getHostName().equals(key.hostname()))
+ .collect(Collectors.toList());
+ if (hosts.isEmpty()) {
+ return Optional.empty();
+ } else {
+ return Optional.of(hosts.get(0));
+ }
+ }
+
+ public void addBoundHost(HostResource host) {
+ mapping.put(host, new LinkedHashSet<>());
+ hostname2host.put(host.getHostName(), host);
+ }
+
+ Map<HostSpec, Set<ClusterMembership>> getHostToServiceSpecMapping() {
+ Map<HostSpec, Set<ClusterMembership>> specMapping = new LinkedHashMap<>();
+ for (Map.Entry<HostResource, Set<ClusterMembership>> entrySet : mapping.entrySet()) {
+ if (!entrySet.getKey().getHost().isMultitenant()) {
+ Optional<ClusterMembership> membership = entrySet.getValue().stream().filter(m -> m != null).findFirst();
+ specMapping.put(new HostSpec(entrySet.getKey().getHostName(), membership), new LinkedHashSet<>(entrySet.getValue()));
+ }
+ }
+ return specMapping;
+ }
+
+ /** A provision logger which forwards to a deploy logger */
+ private static class ProvisionDeployLogger implements ProvisionLogger {
+
+ private final DeployLogger deployLogger;
+
+ public ProvisionDeployLogger(DeployLogger deployLogger) {
+ this.deployLogger = deployLogger;
+ }
+
+ @Override
+ public void log(Level level, String message) {
+ deployLogger.log(level, message);
+ }
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/InstanceResolver.java b/config-model/src/main/java/com/yahoo/vespa/model/InstanceResolver.java
new file mode 100644
index 00000000000..9c761e425a7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/InstanceResolver.java
@@ -0,0 +1,196 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.*;
+import com.yahoo.config.codegen.CNode;
+import com.yahoo.config.codegen.ConfigGenerator;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.codegen.LeafCNode;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.vespa.config.*;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * This class is capable of resolving config from a config model for a given request. It will handle
+ * incompatibilities of the def version in the request and the version of the config classes the model
+ * is using.
+ * </p>
+ * <p>
+ * This class is agnostic of transport protocol and server implementation.
+ * </p>
+ * <p>
+ * Thread safe.
+ * </p>
+ *
+ * @author vegardh
+ * @since 5.1.5
+ */
+// TODO This functionality should be on VespaModel itself, but we don't have a way right now to apply a config override to a ConfigInstance.Builder
+class InstanceResolver {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(InstanceResolver.class.getName());
+
+ /**
+ * Resolves this config key into a correctly typed ConfigInstance using the given config builder.
+ * FIXME: Make private once config overrides are deprecated.?
+ *
+ * @param key a ConfigKey
+ * @param builder a ConfigBuilder to create the instance from.
+ * @param targetDef the def to use
+ * @return the config instance or null of no producer for this found in model
+ */
+ static ConfigInstance resolveToInstance(ConfigKey<?> key, ConfigBuilder builder, InnerCNode targetDef) {
+ ConfigDefinitionKey defKey = new ConfigDefinitionKey(key);
+ try {
+ if (targetDef != null) applyDef(builder, targetDef);
+ ConfigInstance instance = getInstance(defKey, builder.getClass().getClassLoader());
+ Class<? extends ConfigInstance> clazz = instance.getClass();
+ return clazz.getConstructor(new Class<?>[]{builder.getClass()}).newInstance(builder);
+ } catch (Exception e) {
+ throw new ConfigurationRuntimeException(e);
+ }
+ }
+
+ /**
+ * Resolves this config key into a correctly typed ConfigBuilder using the given config model.
+ * FIXME: Make private once config overrides are deprecated.?
+ *
+ * @return the config builder or null if no producer for this found in model
+ */
+ static ConfigBuilder resolveToBuilder(ConfigKey<?> key, VespaModel model, ConfigDefinition targetDef) {
+ if (model == null) return null;
+ ConfigDefinitionKey defKey = new ConfigDefinitionKey(key);
+ ConfigInstance.Builder builder = model.createBuilder(defKey, targetDef);
+ model.getConfig(builder, key.getConfigId());
+ return builder;
+ }
+
+ /**
+ * If some fields on the builder are null now, set them from the def. Do recursively.
+ * <p>
+ * If the targetDef has some schema incompatibilities, they are not handled here
+ * (except logging in some cases), but in ConfigInstance.serialize().
+ *
+ * @param builder a {@link com.yahoo.config.ConfigBuilder}
+ * @param targetDef a config definition
+ * @throws Exception if applying values form config definitions fails
+ */
+ static void applyDef(ConfigBuilder builder, InnerCNode targetDef) throws Exception {
+ for (Map.Entry<String, CNode> e: targetDef.children().entrySet()) {
+ CNode node = e.getValue();
+ if (node instanceof LeafCNode) {
+ setLeafValueIfUnset(targetDef, builder, (LeafCNode)node);
+ } else if (node instanceof InnerCNode) {
+ // Is there a private field on the builder that matches this inner node in the def?
+ if (hasField(builder.getClass(), node.getName())) {
+ Field innerField = builder.getClass().getDeclaredField(node.getName());
+ innerField.setAccessible(true);
+ Object innerFieldVal = innerField.get(builder);
+ if (innerFieldVal instanceof List) {
+ // inner array? Check that list elems are ConfigBuilder
+ List<?> innerList = (List<?>) innerFieldVal;
+ for (Object b : innerList) {
+ if (b instanceof ConfigBuilder) {
+ applyDef((ConfigBuilder) b, (InnerCNode) node);
+ }
+ }
+ } else if (innerFieldVal instanceof ConfigBuilder) {
+ // Struct perhaps
+ applyDef((ConfigBuilder) innerFieldVal, (InnerCNode) node);
+ } else {
+ // Likely a config value mismatch. That is handled in ConfigInstance.serialize() (error message, omit from response.)
+ }
+ }
+ }
+ }
+ }
+
+ private static boolean hasField(Class<?> aClass, String name) {
+ for (Field field : aClass.getDeclaredFields()) {
+ if (name.equals(field.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void setLeafValueIfUnset(InnerCNode targetDef, Object builder, LeafCNode node) throws Exception {
+ if (hasField(builder.getClass(), node.getName())) {
+ Field field = builder.getClass().getDeclaredField(node.getName());
+ field.setAccessible(true);
+ Object val = field.get(builder);
+ if (val==null) {
+ // Not set on builder, if the leaf node has a default value, try the private setter that takes String
+ try {
+ if (node.getDefaultValue()!=null) {
+ Method setter = builder.getClass().getDeclaredMethod(node.getName(), String.class);
+ setter.setAccessible(true);
+ setter.invoke(builder, node.getDefaultValue().getValue());
+ }
+ } catch (Exception e) {
+ log.severe("For config '"+targetDef.getFullName()+"': Unable to apply the default value for field '"+node.getName()+
+ "' to config Builder (where it wasn't set): "+
+ Exceptions.toMessageString(e));
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a ConfigBuilder given a definition key and a payload
+ * @param key The key to use to create the correct builder.
+ * @param payload The payload to populate the builder with.
+ * @return A ConfigBuilder initialized with payload.
+ */
+ static ConfigBuilder createBuilderFromPayload(ConfigDefinitionKey key, VespaModel model, ConfigPayload payload, ConfigDefinition targetDef) {
+ ConfigBuilder builderInstance = model.createBuilder(key, targetDef);
+ if (builderInstance == null || builderInstance instanceof GenericConfig.GenericConfigBuilder) {
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "Creating generic builder for key=" + key);
+ }
+ return new GenericConfig.GenericConfigBuilder(key, new ConfigPayloadBuilder(payload));
+ }
+ ConfigTransformer transformer = new ConfigTransformer(builderInstance.getClass().getDeclaringClass());
+ return transformer.toConfigBuilder(payload);
+ }
+
+ /**
+ * Returns a {@link ConfigInstance} of right type for given key using reflection
+ *
+ * @param cKey a ConfigKey
+ * @return a {@link ConfigInstance} or null if not available in classpath
+ */
+ static ConfigInstance getInstance(ConfigDefinitionKey cKey, ClassLoader instanceLoader) {
+ String className = ConfigGenerator.createClassName(cKey.getName());
+ Class<?> clazz;
+ String fullClassName = packageName(cKey) + "." + className;
+ try {
+ clazz = instanceLoader != null ? instanceLoader.loadClass(fullClassName) : Class.forName(fullClassName);
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ Object i;
+ try {
+ i = clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new ConfigurationRuntimeException(e);
+ }
+ if (!(i instanceof ConfigInstance)) {
+ throw new ConfigurationRuntimeException(fullClassName + " is not a ConfigInstance, can not produce config for the name '" + cKey.getName() + "'.");
+ }
+ return (ConfigInstance) i;
+ }
+
+ static String packageName(ConfigDefinitionKey cKey) {
+ String prefix = "com.yahoo.";
+ return prefix + (cKey.getNamespace().isEmpty() ? CNode.DEFAULT_NAMESPACE : cKey.getNamespace());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/Logd.java b/config-model/src/main/java/com/yahoo/vespa/model/Logd.java
new file mode 100644
index 00000000000..41127f6423b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/Logd.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+/**
+ * There is one logd running on each Vespa host, and one instance of
+ * this class is therefore created by each instance of class {@link
+ * Host}.
+ *
+ * @author gjoranv
+ */
+public class Logd extends AbstractService {
+
+ /**
+ * Creates a new Logd instance.
+ */
+ public Logd(Host host) {
+ super(host, "logd");
+ setProp("clustertype", "hosts");
+ setProp("clustername", "admin");
+ }
+
+ /**
+ * Logd does not need any ports.
+ *
+ * @return The number of ports reserved by the logd
+ */
+ public int getPortCount() { return 0; }
+
+ /**
+ * @return The command used to start logd
+ */
+ public String getStartupCommand() { return "exec sbin/logd"; }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/PlainFormatter.java b/config-model/src/main/java/com/yahoo/vespa/model/PlainFormatter.java
new file mode 100644
index 00000000000..d424f4fa31b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/PlainFormatter.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import java.util.logging.Formatter;
+import java.util.logging.LogRecord;
+
+/**
+ * A log formatter that returns a plain log message only with level, not
+ * including timestamp and method (as java.util.logging.SimpleFormatter).
+ * See bug #1789867.
+ *
+ * @author gjoranv
+ */
+public class PlainFormatter extends Formatter {
+
+ public PlainFormatter() {
+ super();
+ }
+
+ public String format(LogRecord record) {
+ StringBuffer sb = new StringBuffer();
+
+ sb.append(record.getLevel().getName()).append(": ");
+ sb.append(formatMessage(record)).append("\n");
+
+ return sb.toString();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/PortsMeta.java b/config-model/src/main/java/com/yahoo/vespa/model/PortsMeta.java
new file mode 100644
index 00000000000..ea2151f9976
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/PortsMeta.java
@@ -0,0 +1,168 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Track metainformation about the ports of a service.
+ *
+ * @author Vidar Larsen
+ */
+public class PortsMeta implements Serializable {
+ /** A list of all ports. The list elements are lists of strings. */
+ private List<LinkedList<String>> ports;
+
+ /** Remember the rpc admin port offset. */
+ private Integer rpcAdminOffset = null;
+ /** Remember the rpc status port offset. */
+ private Integer rpcStatusOffset = null;
+ /** Remember the https admin port offset. */
+ private Integer httpAdminOffset = null;
+ /** Remember the http status port offset. */
+ private Integer httpStatusOffset = null;
+
+ /** Keep track of the offset to register for, when chaining */
+ private Integer currentOffset;
+
+ /** Create a new PortsMeta object. */
+ public PortsMeta() {
+ ports = new ArrayList<>();
+ }
+
+ /**
+ * Set up the port to tag, for chained usage.
+ * @param offset The relative port to tag.
+ * @return this portsmeta, to allow .tag calls.
+ */
+ public PortsMeta on(int offset) {
+ this.currentOffset = offset;
+ return this;
+ }
+
+ /**
+ * Tag a previously setup port (using 'on') with the specified tag.
+ * @param meta The tag to apply to the current port.
+ * @return this portsmeta, to allow further .tag calls.
+ */
+ public PortsMeta tag(String meta) {
+ if (currentOffset == null) {
+ throw new NullPointerException("No current port offset to tag, use 'on#1'");
+ }
+ return register(currentOffset, meta);
+ }
+
+ /**
+ * Register a given metainfo string to the port at offset.
+ * @param offset 0-based index to identify the port
+ * @param meta a String to be added to the given ports meta set.
+ * @return this for convenient chaining.
+ */
+ private PortsMeta register(int offset, String meta) {
+ // Allocate new LinkedLists on each element up-to-and-including offset
+ for (int i = ports.size(); i <= offset; i++) {
+ ports.add(i, new LinkedList<String>());
+ }
+ ports.get(offset).addFirst(meta);
+
+ return this;
+ }
+
+ /**
+ * Check if the port at a specific offset contains a particular meta attribute.
+ * @param offset The relative port offset
+ * @param meta The meta info we want to check for
+ * @return boolean true if the specific port has registered the meta
+ */
+ public boolean contains(int offset, String meta) {
+ return offset < ports.size() && ports.get(offset).contains(meta);
+ }
+
+ /**
+ * Get the number of ports with registered meta.
+ * @return the number of ports that have been registered.
+ */
+ public int getNumPorts() {
+ return ports.size();
+ }
+ /**
+ * Get an iterator of the Strings registered at the specific point.
+ * @param offset The relative offset to inquire about tags.
+ * @return List of tags.
+ */
+ public List<String> getTagsAt(int offset) {
+ try {
+ return ports.get(offset);
+ } catch (IndexOutOfBoundsException e) {
+ throw new RuntimeException("Trying to get ports meta with offset " + offset +
+ ", which is outside the range 0 to " + ports.size(), e);
+ }
+ }
+
+ /**
+ * Get the offset to the rpc port used for admin.
+ * @return Integer the offset, or null if none set.
+ */
+ public Integer getRpcAdminOffset() {
+ if (rpcAdminOffset == null) {
+ for (int p = 0; p < getNumPorts(); p++) {
+ if (contains(p, "rpc") && contains(p, "admin") && !contains(p, "nc")) {
+ rpcAdminOffset = p;
+ break;
+ }
+ }
+ }
+ return rpcAdminOffset;
+ }
+
+ /**
+ * Get the offset to the rpc port used for status.
+ * @return Integer the offset, or null if none set.
+ */
+ public Integer getRpcStatusOffset() {
+ if (rpcStatusOffset == null) {
+ for (int p = 0; p < getNumPorts(); p++) {
+ if (contains(p, "rpc") && contains(p, "status") && !contains(p, "nc")) {
+ rpcStatusOffset = p;
+ break;
+ }
+ }
+ }
+ return rpcStatusOffset;
+ }
+
+ /**
+ * Get the offset to the http port used for admin.
+ * @return Integer the offset, or null if none set.
+ */
+ public Integer getHttpAdminOffset() {
+ if (httpAdminOffset == null) {
+ for (int p = 0; p < getNumPorts(); p++) {
+ if (contains(p, "http") && contains(p, "admin")) {
+ httpAdminOffset = p;
+ break;
+ }
+ }
+ }
+ return httpAdminOffset;
+ }
+
+ /**
+ * Get the offset to the http port used for status.
+ * @return Integer the offset, or null if none set.
+ */
+ public Integer getHttpStatusOffset() {
+ if (httpStatusOffset == null) {
+ for (int p = 0; p < getNumPorts(); p++) {
+ if (contains(p, "http") && contains(p, "status")) {
+ httpStatusOffset = p;
+ break;
+ }
+ }
+ }
+ return httpStatusOffset;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/RecentLogFilter.java b/config-model/src/main/java/com/yahoo/vespa/model/RecentLogFilter.java
new file mode 100644
index 00000000000..84f33b637fb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/RecentLogFilter.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import java.util.LinkedList;
+import java.util.logging.Filter;
+import java.util.logging.LogRecord;
+
+/**
+ * A filter for log messages that prevents the most recently published messages
+ * to be published again. See bug #461290.
+ *
+ * @author gjoranv
+ */
+public class RecentLogFilter implements Filter {
+
+ LinkedList<String> recent = new LinkedList<>();
+ static final int maxMessages = 6;
+
+ public boolean isLoggable(LogRecord record) {
+ String msg = record.getMessage();
+ if (msg != null && recent.contains(msg)) {
+ return false; // duplicate
+ } else {
+ recent.addLast(msg);
+ if (recent.size() > maxMessages) {
+ recent.removeFirst();
+ }
+ return true; // new message
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/Service.java b/config-model/src/main/java/com/yahoo/vespa/model/Service.java
new file mode 100644
index 00000000000..ccb6397dd46
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/Service.java
@@ -0,0 +1,142 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+
+/**
+ * Representation of a process which runs a service
+ *
+ * @author gjoranv
+ */
+public interface Service extends ConfigProducer {
+
+ /**
+ * Services that should be started by config-sentinel must return
+ * non-null. The returned value will be used in config-sentinel
+ * configuration.
+ * TODO: Should change this to Optional of String
+ */
+ public String getStartupCommand();
+
+ /**
+ * Services that wish that a command should be run before shutdown
+ * should return the command here. The command will be executed
+ * by the config sentinel before sending SIGTERM to the service.
+ * The command is executed without a timeout.
+ */
+ public Optional<String> getPreShutdownCommand();
+
+ /**
+ * Tells if this service should be autostarted by
+ * config-sentinel. Returned value will be used to configure the
+ * config-sentinel.
+ */
+ public boolean getAutostartFlag();
+
+ /**
+ * Tells if this service should be autorestarted by
+ * config-sentinel. Returned value will be used to configure the
+ * config-sentinel.
+ */
+ public boolean getAutorestartFlag();
+
+ /**
+ * Returns the type of service. E.g. the class-name without the
+ * package prefix.
+ */
+ public String getServiceType();
+
+ /**
+ * Returns the name that identifies this service for the config-sentinel.
+ */
+ public String getServiceName();
+
+ /**
+ * Returns the desired base port for this service, or '0' if this
+ * service should use the default port allocation mechanism.
+ *
+ * @return The desired base port for this service.
+ */
+ public int getWantedPort();
+
+ /**
+ * Returns true if the desired base port (returned by
+ * getWantedPort()) for this service is the only allowed base
+ * port.
+ *
+ * @return true if this Service requires the wanted base port.
+ */
+ public boolean requiresWantedPort();
+
+ /**
+ * Returns the number of ports needed by this service.
+ */
+ public int getPortCount();
+
+ /**
+ * Returns a PortsMeta object, giving access to more information
+ * about the different ports of this service.
+ */
+ public PortsMeta getPortsMeta();
+
+ /**
+ * @return the physical host on which this service runs.
+ */
+ public Host getHost();
+
+ /**
+ * Get meta information about service.
+ * @return an instance of {@link com.yahoo.config.model.api.ServiceInfo}
+ */
+ public ServiceInfo getServiceInfo();
+
+ /**
+ * @return The hostname on which this service runs.
+ */
+ public String getHostName();
+
+ /** Optional JVM execution args for this service */
+ public String getJvmArgs();
+
+ /**
+ * Computes and returns the i'th port for this service, based on
+ * this Service's baseport.
+ *
+ * @param i The offset from 'basePort' of the port to return
+ * @return the i'th port relative to the base port.
+ * @throws IllegalStateException if i is out of range.
+ */
+ public int getRelativePort(int i);
+
+ /**
+ * Gets a service property value mapped to the given key
+ * as a String, or the value in <code>defStr</code> if no such key exists.
+ *
+ * @param key a key used for lookup in the service properties
+ * @param defStr default String value returned if no value for key found
+ * @return the associated String value for the given key, or
+ */
+ public String getServicePropertyString(String key, String defStr);
+
+ /**
+ * @return the service health port, to report status to yamas
+ */
+ public int getHealthPort();
+
+ /**
+ *
+ * @return HashMap of default dimensions for metrics.
+ */
+ public HashMap<String,String> getDefaultMetricDimensions();
+
+ /**
+ * Return the Affinity of this service if it has.
+ *
+ * @return The {@link com.yahoo.vespa.model.Affinity} for this service.
+ */
+ public Optional<Affinity> getAffinity();
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/ServiceProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/ServiceProvider.java
new file mode 100644
index 00000000000..29508eda1eb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/ServiceProvider.java
@@ -0,0 +1,11 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+/**
+ * A provider of services (an entity which has its own top-level tag in the services.xml file).
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public interface ServiceProvider {
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/SimpleConfigProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/SimpleConfigProducer.java
new file mode 100644
index 00000000000..3e5f816f583
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/SimpleConfigProducer.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * Some configuration level with no special handling of its own.
+ *
+ * @author arnej27959
+ */
+public final class SimpleConfigProducer<T extends AbstractConfigProducer<?>> extends AbstractConfigProducer<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a new instance
+ *
+ * @param parent parent ConfigProducer.
+ * @param configId name of this instance
+ */
+ public SimpleConfigProducer(AbstractConfigProducer parent, String configId) {
+ super(parent, configId);
+ }
+
+ //Ease access restriction
+ @Override
+ public void addChild(T child) {
+ super.addChild(child);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/VespaConfigModelRegistry.java b/config-model/src/main/java/com/yahoo/vespa/model/VespaConfigModelRegistry.java
new file mode 100644
index 00000000000..6b9aa3fe8c5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/VespaConfigModelRegistry.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.model.ConfigModelRegistry;
+import com.yahoo.config.model.admin.AdminModel;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.vespa.model.builder.xml.dom.*;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder.Networking;
+import com.yahoo.vespa.model.generic.GenericServicesBuilder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Resolves the Vespa config model types
+ *
+ * @author bratseth
+ */
+public class VespaConfigModelRegistry extends ConfigModelRegistry {
+
+ private final List<ConfigModelBuilder> builderList = new ArrayList<>();
+
+ /** Creates a bundled model class registry which forwards unresolved requests to the argument instance */
+ public VespaConfigModelRegistry(ConfigModelRegistry chained) {
+ super(chained);
+ builderList.add(new AdminModel.BuilderV2());
+ builderList.add(new AdminModel.BuilderV4());
+ builderList.add(new DomRoutingBuilder());
+ builderList.add(new DomClientsBuilder());
+ builderList.add(new DomContentBuilder());
+ builderList.add(new ContainerModelBuilder(false, Networking.enable));
+ builderList.add(new GenericServicesBuilder());
+ }
+
+ @Override
+ public Collection<ConfigModelBuilder> resolve(ConfigModelId id) {
+ Set<ConfigModelBuilder> builders = new HashSet<>(chained().resolve(id));
+ for (ConfigModelBuilder builder : builderList) {
+ if (builder.handlesElements().contains(id))
+ builders.add(builder);
+ }
+ return builders;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java b/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java
new file mode 100644
index 00000000000..9a23be1f5c5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/VespaModel.java
@@ -0,0 +1,557 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.ConfigBuilder;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigInstance.Builder;
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.codegen.ConfigGenerator;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.model.ConfigModelRegistry;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.config.model.producer.UserConfigRepo;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.config.subscription.ConfigInstanceUtil;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.ConfigPayloadBuilder;
+import com.yahoo.vespa.config.GenericConfig;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.application.validation.ValidationId;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.builder.VespaModelBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.clients.Clients;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.filedistribution.FileDistributionConfigProducer;
+import com.yahoo.vespa.model.filedistribution.FileDistributor;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+import com.yahoo.vespa.model.routing.Routing;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.utils.internal.ReflectionUtil;
+import com.yahoo.yolean.Exceptions;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import static com.yahoo.text.StringUtilities.quote;
+
+/**
+ * <p>
+ * The root ConfigProducer node for all Vespa systems (there is currently only one).
+ * The main class for building the Vespa model.
+ * </p>
+ * The vespa model starts in an unfrozen state, where children can be added freely,
+ * but no structure dependent information can be used.
+ * When frozen, structure dependent information(such as config id and controller) are
+ * made available, but no additional config producers can be added.
+ *
+ * @author gjoranv
+ */
+public final class VespaModel extends AbstractConfigProducerRoot implements Serializable, Model {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final Logger log = Logger.getLogger(VespaModel.class.getPackage().toString());
+ private ConfigModelRepo configModelRepo = new ConfigModelRepo();
+ private final Optional<ProvisionInfo> info;
+
+ /**
+ * The config id for the root config producer
+ */
+ public static final String ROOT_CONFIGID = "";
+
+ private ApplicationConfigProducerRoot root = null;
+
+ /**
+ * Generic service instances - service clusters which have no specific model
+ */
+ private List<ServiceCluster> serviceClusters = new ArrayList<>();
+
+ private DeployState deployState;
+
+ /** The validation overrides of this. This is never null. */
+ private final ValidationOverrides validationOverrides;
+
+ /** Creates a Vespa Model from internal model types only */
+ public VespaModel(ApplicationPackage app) throws IOException, SAXException {
+ this(app, new NullConfigModelRegistry());
+ }
+
+ /** Creates a Vespa Model from internal model types only */
+ public VespaModel(DeployState deployState) throws IOException, SAXException {
+ this(new NullConfigModelRegistry(), deployState);
+ }
+
+ /**
+ * Constructs vespa model using config given in app
+ *
+ * @param app the application to create a model from
+ * @param configModelRegistry a registry of config model "main" classes which may be used
+ * to instantiate config models
+ */
+ public VespaModel(ApplicationPackage app, ConfigModelRegistry configModelRegistry) throws IOException, SAXException {
+ this(configModelRegistry, new DeployState.Builder().applicationPackage(app).build());
+ }
+
+ /**
+ * Constructs vespa model using config given in app
+ *
+ * @param configModelRegistry a registry of config model "main" classes which may be used
+ * to instantiate config models
+ * @param deployState the global deploy state to use for this model.
+ */
+ public VespaModel(ConfigModelRegistry configModelRegistry, DeployState deployState) throws IOException, SAXException {
+ super("vespamodel");
+ this.deployState = deployState;
+ this.validationOverrides = deployState.validationOverrides();
+ configModelRegistry = new VespaConfigModelRegistry(configModelRegistry);
+ VespaModelBuilder builder = new VespaDomBuilder();
+ root = builder.getRoot(VespaModel.ROOT_CONFIGID, deployState, this);
+ configModelRepo.readConfigModels(deployState, builder, root, configModelRegistry);
+ addServiceClusters(deployState.getApplicationPackage(), builder);
+ setupRouting();
+ log.log(LogLevel.DEBUG, "hostsystem=" + getHostSystem());
+ this.info = Optional.of(createProvisionInfo());
+ getAdmin().addPerHostServices(getHostSystem().getHosts(), deployState.getProperties());
+ freezeModelTopology();
+ root.prepare(configModelRepo);
+ configModelRepo.prepareConfigModels();
+ validateWrapExceptions();
+ this.deployState = null;
+ }
+
+ private ProvisionInfo createProvisionInfo() {
+ HostSystem hostSystem = root.getHostSystem();
+ return ProvisionInfo.withHosts(hostSystem.getHostToServiceSpecMapping().keySet());
+ }
+
+ private void validateWrapExceptions() {
+ try {
+ validate();
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException("Error while validating model:", e);
+ }
+ }
+
+ /** Adds generic application specific clusters of services */
+ private void addServiceClusters(ApplicationPackage app, VespaModelBuilder builder) {
+ for (ServiceCluster sc : builder.getClusters(app, this))
+ serviceClusters.add(sc);
+ }
+
+ private void setupRouting() {
+ root.setupRouting(configModelRepo);
+ }
+
+ /** Returns the one and only HostSystem of this VespaModel */
+ public HostSystem getHostSystem() {
+ return root.getHostSystem();
+ }
+
+ /** Return a collection of all hostnames used in this application */
+ @Override
+ public Set<HostInfo> getHosts() {
+ Set<HostInfo> hosts = new LinkedHashSet<>();
+ for (HostResource host : root.getHostSystem().getHosts()) {
+ if (!host.getHost().isMultitenant()) {
+ hosts.add(host.getHostInfo());
+ }
+ }
+ return hosts;
+ }
+
+ public FileDistributor getFileDistributor() {
+ return root.getFileDistributionConfigProducer().getFileDistributor();
+ }
+
+ /** Returns this models Vespa instance */
+ public ApplicationConfigProducerRoot getVespa() { return root; }
+
+ @Override
+ public boolean allowModelVersionMismatch() {
+ return validationOverrides.allows(ValidationId.configModelVersionMismatch) ||
+ validationOverrides.allows(ValidationId.skipOldConfigModels); // implies this
+ }
+
+ @Override
+ public boolean skipOldConfigModels() {
+ return validationOverrides.allows(ValidationId.skipOldConfigModels);
+ }
+
+ /**
+ * Resolves config of the given type and config id, by first instantiating the correct {@link com.yahoo.config.ConfigInstance.Builder},
+ * calling {@link #getConfig(com.yahoo.config.ConfigInstance.Builder, String)}. The default values used will be those of the config
+ * types in the model.
+ *
+ * @param clazz The type of config
+ * @param configId The config id
+ * @return A config instance of the given type
+ */
+ public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> clazz, String configId) {
+ try {
+ ConfigInstance.Builder builder = newBuilder(clazz);
+ getConfig(builder, configId);
+ return newConfigInstance(clazz, builder);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ /**
+ * Populates an instance of configClass with config produced by configProducer.
+ */
+ public static <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ConfigProducer configProducer) {
+ try {
+ Builder builder = newBuilder(configClass);
+ populateConfigBuilder(builder, configProducer);
+ return newConfigInstance(configClass, builder);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed getting config for class " + configClass.getName(), e);
+ }
+ }
+
+ private static <CONFIGTYPE extends ConfigInstance> CONFIGTYPE newConfigInstance(Class<CONFIGTYPE> configClass, Builder builder)
+ throws NoSuchMethodException, InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException {
+
+ Constructor<CONFIGTYPE> constructor = configClass.getConstructor(builder.getClass());
+ return constructor.newInstance(builder);
+ }
+
+ private static Builder newBuilder(Class<? extends ConfigInstance> configClass)
+ throws ClassNotFoundException, InstantiationException, IllegalAccessException {
+
+ Class builderClazz = configClass.getClassLoader().loadClass(configClass.getName() + "$Builder");
+ return (Builder)builderClazz.newInstance();
+ }
+
+ /**
+ * Throw if the config id does not exist in the model.
+ *
+ * @param configId a config id
+ */
+ protected void checkId(String configId) {
+ if ( ! id2producer.containsKey(configId)) {
+ log.log(LogLevel.DEBUG, "Invalid config id: " + configId);
+ }
+ }
+
+ /**
+ * Resolves config for a given config id and populates the given builder with the config.
+ *
+ * @param builder a configinstance builder
+ * @param configId the config id for the config client
+ * @return the builder if a producer was found, and it did apply config, null otherwise
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public ConfigInstance.Builder getConfig(ConfigInstance.Builder builder, String configId) {
+ checkId(configId);
+ Optional<ConfigProducer> configProducer = getConfigProducer(configId);
+ if ( ! configProducer.isPresent()) return null;
+
+ populateConfigBuilder(builder, configProducer.get());
+ return builder;
+ }
+
+ private static void populateConfigBuilder(Builder builder, ConfigProducer configProducer) {
+ boolean found = configProducer.cascadeConfig(builder);
+ boolean foundOverride = configProducer.addUserConfig(builder);
+ if (logDebug()) {
+ log.log(LogLevel.DEBUG, "Trying to get config for " + builder.getClass().getDeclaringClass().getName() +
+ " for config id " + quote(configProducer.getConfigId()) +
+ ", found=" + found + ", foundOverride=" + foundOverride);
+ }
+ }
+
+ /** TODO: Remove once configserver has switched to using {@link VespaModel#getConfig(ConfigKey, ConfigDefinition, ConfigPayload)} instead. **/
+ public ConfigPayload getConfig(ConfigKey configKey, InnerCNode targetDef, ConfigPayload userOverride) throws IOException {
+ return getConfig(configKey, targetDef == null ? null : new ConfigDefinition(targetDef), userOverride);
+ }
+
+ /**
+ * Resolve config for a given key and a def. Apply an override if given.
+ *
+ * @param configKey The key to resolve.
+ * @param targetDef The def file to use for the schema.
+ * @param userOverride A user override that should be applied to the config
+ * @return The payload as a list of strings
+ */
+ @Override
+ public ConfigPayload getConfig(ConfigKey configKey, com.yahoo.vespa.config.buildergen.ConfigDefinition targetDef, ConfigPayload userOverride) throws IOException {
+ ConfigBuilder builder = InstanceResolver.resolveToBuilder(configKey, this, targetDef);
+ if (builder != null) {
+ log.log(LogLevel.DEBUG, () -> "Found builder for " + configKey);
+ // Support deprecated configs/ user override
+ applyConfigsOverride(configKey, builder, userOverride, targetDef);
+ ConfigPayload payload;
+ InnerCNode innerCNode = targetDef != null ? targetDef.getCNode() : null;
+ if (builder instanceof GenericConfig.GenericConfigBuilder) {
+ payload = getConfigFromGenericBuilder(builder);
+ } else {
+ payload = getConfigFromBuilder(configKey, builder, innerCNode);
+ }
+ return (innerCNode != null) ? payload.applyDefaultsFromDef(innerCNode) : payload;
+ }
+ return null;
+ }
+
+ private ConfigPayload getConfigFromBuilder(ConfigKey configKey, ConfigBuilder builder, InnerCNode targetDef) {
+ try {
+ ConfigInstance instance = InstanceResolver.resolveToInstance(configKey, builder, targetDef);
+ log.log(LogLevel.DEBUG, () -> "getConfigFromBuilder for " + configKey + ",instance=" + instance);
+ return ConfigPayload.fromInstance(instance);
+ } catch (ConfigurationRuntimeException e) {
+ // This can happen in cases where services ask for config that no longer exist before they have been able
+ // to reconfigure themselves. This happens for instance whenever jdisc reconfigures itself until
+ // ticket 6599572 is fixed. When that happens, consider propagating a full error rather than empty payload
+ // back to the client.
+ log.log(LogLevel.INFO, "Error resolving instance for key '" + configKey + "', returning empty config: " + Exceptions.toMessageString(e));
+ return ConfigPayload.fromBuilder(new ConfigPayloadBuilder());
+ }
+ }
+
+ private ConfigPayload getConfigFromGenericBuilder(ConfigBuilder builder) throws IOException {
+ return ((GenericConfig.GenericConfigBuilder) builder).getPayload();
+ }
+
+ private void applyConfigsOverride(ConfigKey configKey, ConfigBuilder builder, ConfigPayload userOverride, ConfigDefinition targetDef) {
+ if (userOverride != null) {
+ ConfigBuilder override = InstanceResolver.createBuilderFromPayload(new ConfigDefinitionKey(configKey), this, userOverride, targetDef);
+ ConfigInstanceUtil.setValues(builder, override);
+ }
+ }
+
+ @Override
+ public Set<ConfigKey<?>> allConfigsProduced() {
+ Set<ConfigKey<?>> keySet = new LinkedHashSet<>();
+ for (ConfigProducer producer : id2producer().values()) {
+ keySet.addAll(configsProduced(producer));
+ }
+ return keySet;
+ }
+
+ public ConfigInstance.Builder createBuilder(ConfigDefinitionKey key, ConfigDefinition targetDef) {
+ String className = ConfigGenerator.createClassName(key.getName());
+ Class<?> clazz;
+
+ final String fullClassName = InstanceResolver.packageName(key) + "." + className;
+ final String builderName = fullClassName + "$Builder";
+ final String producerName = fullClassName + "$Producer";
+ ClassLoader classLoader = getConfigClassLoader(producerName);
+ if (classLoader == null) {
+ classLoader = getClass().getClassLoader();
+ if (logDebug()) {
+ log.log(LogLevel.DEBUG, "No producer found to get classloader from for " + fullClassName + ". Using default");
+ }
+ }
+ try {
+ clazz = classLoader.loadClass(builderName);
+ } catch (ClassNotFoundException e) {
+ if (logDebug()) {
+ log.log(LogLevel.DEBUG, "Tried to load " + builderName + ", not found, trying with generic builder");
+ }
+ // TODO: Enable config compiler when configserver is using new API.
+ // ConfigCompiler compiler = new LazyConfigCompiler(Files.createTempDir());
+ // return compiler.compile(targetDef.generateClass()).newInstance();
+ return new GenericConfig.GenericConfigBuilder(key, new ConfigPayloadBuilder());
+ }
+ Object i;
+ try {
+ i = clazz.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new ConfigurationRuntimeException(e);
+ }
+ if (!(i instanceof ConfigInstance.Builder)) {
+ throw new ConfigurationRuntimeException(fullClassName + " is not a ConfigInstance.Builder, can not produce config for the name '" + key.getName() + "'.");
+ }
+ return (ConfigInstance.Builder) i;
+ }
+
+ private static boolean logDebug() {
+ return log.isLoggable(LogLevel.DEBUG);
+ }
+
+ /**
+ * The set of all config ids present
+ * @return set of config ids
+ */
+ public Set<String> allConfigIds() {
+ return id2producer.keySet();
+ }
+
+ @Override
+ public void distributeFiles(FileDistribution fileDistribution) {
+ getFileDistributor().sendDeployedFiles(fileDistribution);
+ }
+
+ @Override
+ public void reloadDeployFileDistributor(FileDistribution fileDistribution) {
+ getFileDistributor().reloadDeployFileDistributor(fileDistribution);
+ }
+
+ @Override
+ public Optional<ProvisionInfo> getProvisionInfo() {
+ return info;
+ }
+
+ private static Set<ConfigKey<?>> configsProduced(ConfigProducer cp) {
+ Set<ConfigKey<?>> ret = ReflectionUtil.configsProducedByInterface(cp.getClass(), cp.getConfigId());
+ UserConfigRepo userConfigs = cp.getUserConfigs();
+ for (ConfigDefinitionKey userKey : userConfigs.configsProduced()) {
+ ret.add(new ConfigKey<>(userKey.getName(), cp.getConfigId(), userKey.getNamespace()));
+ }
+ return ret;
+ }
+
+ @Override
+ public DeployState getDeployState() {
+ if (deployState == null) {
+ throw new IllegalStateException("Cannot call getDeployState() once model has been built");
+ }
+ return deployState;
+ }
+
+ /**
+ * @return an unmodifiable copy of the set of configIds in this VespaModel.
+ */
+ public Set<String> getConfigIds() {
+ return Collections.unmodifiableSet(id2producer.keySet());
+ }
+
+ /**
+ * Returns the admin component of the vespamodel.
+ *
+ * @return Admin
+ */
+ public Admin getAdmin() {
+ return root.getAdmin();
+ }
+
+ /**
+ * Adds the descendant (at any depth level), so it can be looked up
+ * on configId in the Map.
+ *
+ * @param configId the id to register with, not necessarily equal to descendant.getConfigId().
+ * @param descendant The configProducer descendant to add
+ */
+ public void addDescendant(String configId, AbstractConfigProducer descendant) {
+ if (id2producer.containsKey(configId)) {
+ throw new RuntimeException
+ ("Config ID '" + configId + "' cannot be reserved by an instance of class '" +
+ descendant.getClass().getName() +
+ "' since it is already used by an instance of class '" +
+ id2producer.get(configId).getClass().getName() +
+ "'. (This is commonly caused by service/node index " +
+ "collisions in the config.)");
+ }
+ id2producer.put(configId, descendant);
+ }
+
+ /**
+ * Writes MODEL.cfg files for all config producers.
+ *
+ * @param baseDirectory dir to write files to
+ */
+ public void writeFiles(File baseDirectory) throws IOException {
+ super.writeFiles(baseDirectory);
+ for (ConfigProducer cp : id2producer.values()) {
+ try {
+ File destination = new File(baseDirectory, cp.getConfigId().replace("/", File.separator));
+ cp.writeFiles(destination);
+ } catch (IOException e) {
+ throw new IOException(cp.getConfigId() + ": " + e.getMessage());
+ }
+ }
+ }
+
+ public Clients getClients() {
+ return configModelRepo.getClients();
+ }
+
+ /** Returns all search clusters, both in Search and Content */
+ public List<AbstractSearchCluster> getSearchClusters() {
+ return Content.getSearchClusters(configModelRepo());
+ }
+
+ /** Returns a map of content clusters by ID */
+ public Map<String, ContentCluster> getContentClusters() {
+ Map<String, ContentCluster> clusters = new LinkedHashMap<>();
+ for (Content model : configModelRepo.getModels(Content.class)) {
+ clusters.put(model.getId(), model.getCluster());
+ }
+ return Collections.unmodifiableMap(clusters);
+ }
+
+ /** Returns a map of container clusters by ID */
+ public Map<String, ContainerCluster> getContainerClusters() {
+ Map<String, ContainerCluster> clusters = new LinkedHashMap<>();
+ for (ContainerModel model : configModelRepo.getModels(ContainerModel.class)) {
+ clusters.put(model.getId(), model.getCluster());
+ }
+ return Collections.unmodifiableMap(clusters);
+ }
+
+ /** Returns the routing config model. This might be null. */
+ public Routing getRouting() {
+ return configModelRepo.getRouting();
+ }
+
+ public FileDistributionConfigProducer getFileDistributionConfigProducer() {
+ return root.getFileDistributionConfigProducer();
+ }
+
+ /** The clusters of application specific generic services */
+ public List<ServiceCluster> serviceClusters() {
+ return serviceClusters;
+ }
+
+ /** Returns an unmodifiable view of the mapping of config id to {@link ConfigProducer} */
+ public Map<String, ConfigProducer> id2producer() {
+ return Collections.unmodifiableMap(id2producer);
+ }
+
+ /**
+ * @return this root's model repository
+ */
+ public ConfigModelRepo configModelRepo() {
+ return configModelRepo;
+ }
+
+ @Override
+ public DeployLogger deployLogger() {
+ return getDeployState().getDeployLogger();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java b/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java
new file mode 100644
index 00000000000..0dccc2c9750
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/VespaModelFactory.java
@@ -0,0 +1,196 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.google.inject.Inject;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.ConfigModelRegistry;
+import com.yahoo.config.model.MapConfigModelRegistry;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ConfigModelPlugin;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.api.ModelContext;
+import com.yahoo.config.model.api.ModelCreateResult;
+import com.yahoo.config.model.api.ModelFactory;
+import com.yahoo.config.model.application.provider.ApplicationPackageXmlFilesValidator;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.provision.HostsXmlProvisioner;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.config.VespaVersion;
+import com.yahoo.vespa.model.application.validation.Validation;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Factory for creating {@link VespaModel} instances.
+ *
+ * @author lulf
+ */
+public class VespaModelFactory implements ModelFactory {
+
+ private static final Logger log = Logger.getLogger(VespaModelFactory.class.getName());
+ private final ConfigModelRegistry configModelRegistry;
+ private final Zone zone;
+
+ @Inject
+ public VespaModelFactory(ComponentRegistry<ConfigModelPlugin> pluginRegistry, Zone zone) {
+ List<ConfigModelBuilder> modelBuilders = new ArrayList<>();
+ for (ConfigModelPlugin plugin : pluginRegistry.allComponents()) {
+ if (plugin instanceof ConfigModelBuilder) {
+ modelBuilders.add((ConfigModelBuilder) plugin);
+ }
+ }
+ this.configModelRegistry = new MapConfigModelRegistry(modelBuilders);
+ this.zone = zone;
+ }
+
+ public VespaModelFactory(ConfigModelRegistry configModelRegistry) {
+ if (configModelRegistry == null) {
+ this.configModelRegistry = new NullConfigModelRegistry();
+ log.info("Will not load config models from plugins, as no registry is available");
+ } else {
+ this.configModelRegistry = configModelRegistry;
+ }
+ this.zone = Zone.defaultZone();
+ }
+
+ @Override
+ public Version getVersion() {
+ return Version.fromIntValues(VespaVersion.major, VespaVersion.minor, VespaVersion.micro);
+ }
+
+ @Override
+ public Model createModel(ModelContext modelContext) {
+ return buildModel(createDeployState(modelContext));
+ }
+
+ private VespaModel buildModel(DeployState deployState) {
+ try {
+ return new VespaModel(configModelRegistry, deployState);
+ } catch (IOException | SAXException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private DeployState createDeployState(ModelContext modelContext) {
+ DeployState.Builder builder = new DeployState.Builder()
+ .applicationPackage(modelContext.applicationPackage())
+ .deployLogger(modelContext.deployLogger())
+ .configDefinitionRepo(modelContext.configDefinitionRepo())
+ .fileRegistry(modelContext.getFileRegistry())
+ .permanentApplicationPackage(modelContext.permanentApplicationPackage())
+ .properties(createDeployProperties(modelContext.properties()))
+ .modelHostProvisioner(createHostProvisioner(modelContext))
+ .rotations(modelContext.properties().rotations())
+ .zone(zone);
+ modelContext.previousModel().ifPresent(builder::previousModel);
+ return builder.build();
+ }
+
+ private DeployProperties createDeployProperties(ModelContext.Properties properties) {
+ return new DeployProperties.Builder()
+ .applicationId(properties.applicationId())
+ .configServerSpecs(properties.configServerSpecs())
+ .multitenant(properties.multitenant())
+ .hostedVespa(properties.hostedVespa())
+ .vespaVersion(getVersion())
+ .zone(properties.zone())
+ .build();
+ }
+
+
+ private static HostProvisioner createHostProvisioner(ModelContext modelContext) {
+ if (isHostedVespaRoutingApplication(modelContext)) {
+ //TODO: This belongs in HostedVespaProvisioner.
+ //Added here for now since com.yahoo.config.model.api.HostProvisioner is not created per application,
+ //and allocation is independent of ApplicationPackage.
+ return new HostsXmlProvisioner(hostsXml(modelContext));
+ } else {
+ return modelContext.hostProvisioner().orElse(
+ DeployState.getDefaultModelHostProvisioner(modelContext.applicationPackage()));
+ }
+ }
+
+ private static Reader hostsXml(ModelContext modelContext) {
+ Reader hosts = modelContext.applicationPackage().getHosts();
+ if (hosts == null) {
+ //TODO: throw InvalidApplicationException directly. Not possible now, as it resides in the configserver module.
+ //SessionPreparer maps IllegalArgumentException -> InvalidApplicationException
+ throw new IllegalArgumentException("Hosted vespa routing application must use " + ApplicationPackage.HOSTS
+ + " to allocate hosts.");
+ }
+ return hosts;
+ }
+
+ private static boolean isHostedVespaRoutingApplication(ModelContext modelContext) {
+ ApplicationId id = modelContext.properties().applicationId();
+ return modelContext.properties().hostedVespa() && id.isHostedVespaRoutingApplication();
+ }
+
+ @Override
+ public ModelCreateResult createAndValidateModel(ModelContext modelContext, boolean ignoreValidationErrors) {
+ if (modelContext.appDir().isPresent()) {
+ ApplicationPackageXmlFilesValidator validator =
+ ApplicationPackageXmlFilesValidator.createDefaultXMLValidator(modelContext.appDir().get(),
+ modelContext.deployLogger(),
+ modelContext.vespaVersion());
+ try {
+ validator.checkApplication();
+ ApplicationPackageXmlFilesValidator.checkIncludedDirs(modelContext.applicationPackage());
+ } catch (IllegalArgumentException e) {
+ rethrowUnlessIgnoreErrors(e, ignoreValidationErrors);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ } else {
+ validateXML(modelContext.applicationPackage(), modelContext.deployLogger(), ignoreValidationErrors);
+ }
+ DeployState deployState = createDeployState(modelContext);
+ VespaModel model = buildModel(deployState);
+ List<ConfigChangeAction> changeActions = validateModel(model, deployState, ignoreValidationErrors);
+ return new ModelCreateResult(model, changeActions);
+ }
+
+ private void validateXML(ApplicationPackage applicationPackage, DeployLogger deployLogger, boolean ignoreValidationErrors) {
+ try {
+ applicationPackage.validateXML(deployLogger);
+ } catch (IllegalArgumentException e) {
+ rethrowUnlessIgnoreErrors(e, ignoreValidationErrors);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List<ConfigChangeAction> validateModel(VespaModel model, DeployState deployState, boolean ignoreValidationErrors) {
+ try {
+ deployState.getApplicationPackage().validateXML(deployState.getDeployLogger());
+ return Validation.validate(model, ignoreValidationErrors, deployState);
+ } catch (IllegalArgumentException e) {
+ rethrowUnlessIgnoreErrors(e, ignoreValidationErrors);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return new ArrayList<>();
+ }
+
+ private static void rethrowUnlessIgnoreErrors(IllegalArgumentException e, boolean ignoreValidationErrors) {
+ if (!ignoreValidationErrors) {
+ throw e;
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/AbstractMonitoringSystem.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/AbstractMonitoringSystem.java
new file mode 100644
index 00000000000..155dcca03d9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/AbstractMonitoringSystem.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import java.util.Objects;
+
+/**
+ *
+ * Represents an abstract monitoring service
+ *
+ * @author musum
+ * @since 5.1.20
+ *
+*/
+class AbstractMonitoringSystem implements MonitoringSystem {
+
+ private final Integer interval;
+ private final String clustername;
+
+ public AbstractMonitoringSystem(String clustername, Integer interval) {
+ Objects.requireNonNull(clustername);
+ Objects.requireNonNull(interval);
+ this.clustername = clustername;
+ this.interval = interval;
+ }
+
+ @Override
+ public Integer getInterval() {
+ return interval;
+ }
+
+ @Override
+ public Integer getIntervalSeconds() {
+ return interval * 60;
+ }
+
+ @Override
+ public String getClustername() {
+ return clustername;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java
new file mode 100644
index 00000000000..38a1e59433f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/Admin.java
@@ -0,0 +1,248 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.cloud.config.ZookeepersConfig;
+import com.yahoo.config.model.api.ConfigServerSpec;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.cloud.config.SlobroksConfig;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.model.*;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.filedistribution.FileDistributionConfigProducer;
+import com.yahoo.vespa.model.filedistribution.FileDistributor;
+import com.yahoo.vespa.model.filedistribution.FileDistributorService;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.yahoo.vespa.model.HostResource;
+
+/**
+ * This is the admin pseudo-plugin of the Vespa model, responsible for
+ * creating all admin services.
+ *
+ * @author gjoranv
+ */
+public class Admin extends AbstractConfigProducer implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Yamas yamas;
+ private final Map<String,MetricsConsumer> metricsConsumers;
+ private final List<Configserver> configservers = new ArrayList<>();
+
+ private final List<Slobrok> slobroks = new ArrayList<>();
+ private Configserver defaultConfigserver;
+ private Logserver logserver;
+
+ /**
+ * The single cluster controller cluster shared by all content clusters by default when not multitenant.
+ * If multitenant, this is null.
+ */
+ private ContainerCluster clusterControllers;
+
+ private ZooKeepersConfigProvider zooKeepersConfigProvider;
+ private FileDistributionConfigProducer fileDistribution;
+ private final boolean multitenant;
+
+ public Admin(AbstractConfigProducer parent, Yamas yamas, Map<String, MetricsConsumer> metricsConsumers, boolean multitenant) {
+ super(parent, "admin");
+ this.yamas = yamas;
+ this.metricsConsumers = metricsConsumers;
+ this.multitenant = multitenant;
+ }
+
+ public Configserver getConfigserver() {
+ return defaultConfigserver;
+ }
+
+ /** Returns the configured yamas end point. Is null if yamas is not configured */
+ public Yamas getYamas() {
+ return yamas;
+ }
+
+ /** Returns the configured userMetricConsumers. Null if not configured */
+ public Map<String, MetricsConsumer> getUserMetricsConsumers(){
+ return metricsConsumers;
+ }
+
+ /** Returns a list of all config servers */
+ public List<Configserver> getConfigservers() {
+ return configservers;
+ }
+
+ public List<ConfigServerSpec> getConfigServerSpecs() {
+ List<ConfigServerSpec> serverSpecs = new ArrayList<>();
+ for (Configserver server : getConfigservers()) {
+ serverSpecs.add(server.getConfigServerSpec());
+ }
+ return serverSpecs;
+ }
+
+ public void removeSlobroks() { slobroks.clear(); }
+
+ /** Returns an immutable list of the slobroks in this */
+ public List<Slobrok> getSlobroks() { return Collections.unmodifiableList(slobroks); }
+
+ public void setLogserver(Logserver logserver) { this.logserver = logserver; }
+
+ public Logserver getLogserver() { return logserver; }
+
+ public void addConfigservers(List<Configserver> configservers) {
+ this.configservers.addAll(configservers);
+ if (this.configservers.size() > 0) {
+ this.defaultConfigserver = configservers.get(0);
+ }
+ this.zooKeepersConfigProvider = new ZooKeepersConfigProvider(configservers);
+ }
+
+ public void addSlobroks(List<Slobrok> slobroks) {
+ this.slobroks.addAll(slobroks);
+ }
+
+ public ContainerCluster getClusterControllers() { return clusterControllers; }
+
+ public void setClusterControllers(ContainerCluster clusterControllers) {
+ if (multitenant) throw new RuntimeException("Should not use admin cluster controller in a multitenant environment");
+ this.clusterControllers = clusterControllers;
+ }
+
+ public ZooKeepersConfigProvider getZooKeepersConfigProvider() {
+ return zooKeepersConfigProvider;
+ }
+
+ public void getConfig(LogdConfig.Builder builder) {
+ builder.
+ logserver(new LogdConfig.Logserver.Builder().
+ host(logserver.getHostName()).
+ port(logserver.getRelativePort(1)));
+ }
+
+ public void getConfig(SlobroksConfig.Builder builder) {
+ for (Slobrok slob : slobroks) {
+ builder.
+ slobrok(new SlobroksConfig.Slobrok.Builder().
+ connectionspec(slob.getConnectionSpec()));
+ }
+ }
+
+ public void getConfig(ZookeepersConfig.Builder builder) {
+ zooKeepersConfigProvider.getConfig(builder);
+ }
+
+ public void setFileDistribution(FileDistributionConfigProducer fileDistribution) {
+ this.fileDistribution = fileDistribution;
+ }
+
+ public FileDistributionConfigProducer getFileDistributionConfigProducer() {
+ return fileDistribution;
+ }
+
+ public List<HostResource> getClusterControllerHosts() {
+ List<HostResource> hosts = new ArrayList<>();
+ if (multitenant) {
+ if (logserver != null)
+ hosts.add(logserver.getHostResource());
+ } else {
+ for (Configserver configserver : getConfigservers()) {
+ hosts.add(configserver.getHostResource());
+ }
+ }
+ return hosts;
+ }
+
+ /**
+ * Adds services to all hosts in the system.
+ */
+ public void addPerHostServices(List<HostResource> hosts, DeployProperties properties) {
+ if (slobroks.isEmpty()) // TODO: Move to caller
+ slobroks.addAll(createDefaultSlobrokSetup());
+ for (HostResource host : hosts) {
+ if (!host.getHost().isMultitenant()) {
+ addCommonServices(host, properties);
+ }
+ }
+ }
+ private void addCommonServices(HostResource host, DeployProperties properties) {
+ addConfigSentinel(host, properties.applicationId(), properties.zone());
+ addLogd(host);
+ addConfigProxy(host);
+ addFileDistribution(host);
+ }
+
+ private void addConfigSentinel(HostResource host, ApplicationId applicationId, Zone zone) {
+ ConfigSentinel configSentinel = new ConfigSentinel(host.getHost(), applicationId, zone);
+ addAndInitializeService(host, configSentinel);
+ host.getHost().setConfigSentinel(configSentinel);
+ }
+
+ private void addLogd(HostResource host) {
+ addAndInitializeService(host, new Logd(host.getHost()));
+ }
+
+ private void addConfigProxy(HostResource host) {
+ addAndInitializeService(host, new ConfigProxy(host.getHost()));
+ }
+
+ public void addAndInitializeService(HostResource host, AbstractService service) {
+ service.setHostResource(host);
+ service.initService();
+ }
+
+ private void addFileDistribution(HostResource host) {
+ FileDistributor fileDistributor = fileDistribution.getFileDistributor();
+ HostResource deployHost = getHostSystem().getHostByHostname(fileDistributor.fileSourceHost());
+ if (deployHostIsMissing(deployHost)) {
+ throw new RuntimeException("Could not find host in the application's host system: '" +
+ fileDistributor.fileSourceHost() + "'. Hostsystem=" + getHostSystem());
+ }
+
+ FileDistributorService fds = new FileDistributorService(fileDistribution, host.getHost().getHostName(),
+ fileDistribution.getFileDistributor(), fileDistribution.getOptions(), host == deployHost);
+ fds.setHostResource(host);
+ fds.initService();
+ fileDistribution.addFileDistributionService(host.getHost(), fds);
+ }
+
+ private boolean deployHostIsMissing(HostResource deployHost) {
+ return !multitenant && deployHost == null;
+ }
+
+ // If not configured by user: Use default setup: max 3 slobroks, 1 on the default configserver host
+ private List<Slobrok> createDefaultSlobrokSetup() {
+ List<HostResource> hosts = getHostSystem().getHosts();
+ List<Slobrok> slobs = new ArrayList<>();
+ if (logserver != null) {
+ Slobrok slobrok = new Slobrok(this, 0);
+ addAndInitializeService(logserver.getHostResource(), slobrok);
+ slobs.add(slobrok);
+ }
+
+ int n = 0;
+ while ((n < hosts.size()) && (slobs.size() < 3)) {
+ HostResource host = hosts.get(n);
+ if ((logserver== null || host != logserver.getHostResource()) && ! host.getHost().isMultitenant()) {
+ Slobrok newSlobrok = new Slobrok(this, slobs.size());
+ addAndInitializeService(host, newSlobrok);
+ slobs.add(newSlobrok);
+ }
+ n++;
+ }
+ int j = 0;
+ for (Slobrok s : slobs) {
+ s.setProp("index", j);
+ j++;
+ }
+ return slobs;
+ }
+
+ public boolean multitenant() {
+ return multitenant;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/Configserver.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/Configserver.java
new file mode 100644
index 00000000000..d12ab40835d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/Configserver.java
@@ -0,0 +1,145 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import java.util.logging.Logger;
+
+import com.yahoo.config.model.api.ConfigServerSpec;
+import com.yahoo.log.LogLevel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.AbstractService;
+
+/**
+ * Represents a Configserver. There may be one or more Configservers in a
+ * Vespa system.
+ *
+ * NOTE: The Configserver is not started by the config system, and
+ * does not receive any config. It's included here so we know what host
+ * it runs on, and to give an error message if another service tries
+ * to reserve the ports it is using.
+ *
+ * @author gjoranv
+ */
+public class Configserver extends AbstractService {
+ private static final long serialVersionUID = 1L;
+ public static final int defaultPort = 19070;
+ private static final Logger log = Logger.getLogger(Configserver.class.getName());
+
+ public Configserver(AbstractConfigProducer parent, String name) {
+ super(parent, name);
+ portsMeta.on(0).tag("rpc").tag("config");
+ portsMeta.on(1).tag("http").tag("config").tag("state");
+ setProp("clustertype", "admin");
+ setProp("clustername", "admin");
+ monitorService();
+ }
+
+ /**
+ * Returns the desired base port for this service.
+ */
+ public int getWantedPort() {
+ try {
+ // TODO: Provide configserver port as argument when creating this service instead
+ Process process = new ProcessBuilder(Defaults.getDefaults().vespaHome() + "libexec/vespa/vespa-config.pl", "-configserverport").start();
+ InputStream in = process.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ return Integer.parseInt(reader.readLine().trim());
+ } catch (Exception exception) {
+ log.log(LogLevel.DEBUG, "Error reading port from script, using " + defaultPort);
+ return defaultPort;
+ }
+ }
+
+ /**
+ * The desired base port is the only allowed base port.
+ * @return 'true' always
+ */
+ public boolean requiresWantedPort() {
+ return getId() < 2;
+ }
+
+ /**
+ * @return the number of ports needed by the configserver.
+ */
+ public int getPortCount() { return 2; }
+
+ /**
+ * The configserver is not started by the config system!
+ */
+ public boolean getAutostartFlag() { return false; }
+
+ /**
+ * The configserver is not started by the config system!
+ */
+ public boolean getAutorestartFlag() { return false; }
+
+ private int getConfigServerRpcPort() {
+ return getRelativePort(0);
+ }
+
+ private int getConfigServerHttpPort() {
+ return getRelativePort(1);
+ }
+
+ public ConfigServerSpec getConfigServerSpec() {
+ return new Spec(getHostName(), getConfigServerRpcPort(), getConfigServerHttpPort(), ZooKeepersConfigProvider.zkPort);
+ }
+
+ @Override
+ public int getHealthPort() {
+ return getRelativePort(1);
+ }
+
+ // TODO: Remove this implementation when we are on Hosted Vespa.
+ public static class Spec implements ConfigServerSpec {
+ private final String hostName;
+ private final int configServerPort;
+ private final int httpPort;
+ private final int zooKeeperPort;
+ public String getHostName() {
+ return hostName;
+ }
+
+ public int getConfigServerPort() {
+ return configServerPort;
+ }
+
+ public int getHttpPort() {
+ return httpPort;
+ }
+
+ public int getZooKeeperPort() {
+ return zooKeeperPort;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ConfigServerSpec) {
+ ConfigServerSpec other = (ConfigServerSpec)o;
+
+ return hostName.equals(other.getHostName()) &&
+ configServerPort == other.getConfigServerPort() &&
+ httpPort == other.getHttpPort() &&
+ zooKeeperPort == other.getZooKeeperPort();
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return hostName.hashCode();
+ }
+
+ public Spec(String hostName, int configServerPort, int httpPort, int zooKeeperPort) {
+ this.hostName = hostName;
+ this.configServerPort = configServerPort;
+ this.httpPort = httpPort;
+ this.zooKeeperPort = zooKeeperPort;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/DefaultMetricConsumers.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/DefaultMetricConsumers.java
new file mode 100644
index 00000000000..6082ca9f72d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/DefaultMetricConsumers.java
@@ -0,0 +1,294 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class to set up the default metrics for all services to be forwarded to Yamas
+ *
+ * @author <a href="mailto:trygve@yahoo-inc.com">Trygve Bolsø Berdal</a>
+ */
+public class DefaultMetricConsumers {
+
+ /**
+ * Populates a map of with consumer as key and metrics for that consumer as value. The metrics
+ * are to be forwarded to consumers (ymon and yamas are the options at the moment).
+ *
+ * @return A map of default metric consumers and default metrics for that consumer.
+ */
+ public Map<String, MetricsConsumer> getDefaultMetricsConsumers() {
+ Map<String, MetricsConsumer> metricsConsumers = new LinkedHashMap<>();
+ metricsConsumers.put("yamas", getDefaultYamasConsumer());
+ metricsConsumers.put("ymon", getDefaultYmonConsumer());
+ return metricsConsumers;
+ }
+
+ private MetricsConsumer getDefaultYmonConsumer(){
+ Map<String, Metric> metricMap = new LinkedHashMap<>();
+ for (Metric metric : commonMetrics()) {
+ metricMap.put(metric.getName(), metric);
+ }
+
+ return new MetricsConsumer("ymon", metricMap);
+ }
+
+ private MetricsConsumer getDefaultYamasConsumer(){
+ // include common metrics
+ List<Metric> metrics = commonMetrics();
+
+ //Search node
+ // jobs
+ metrics.add(new Metric("content.proton.documentdb.job.total.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.attribute_flush.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.memory_index_flush.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.disk_index_fusion.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.document_store_flush.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.document_store_compact.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.bucket_move.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.lid_space_compact.average"));
+ metrics.add(new Metric("content.proton.documentdb.job.removed_documents_prune.average"));
+
+ // lid space
+ metrics.add(new Metric("content.proton.documentdb.ready.lid_space.lid_bloat_factor.average"));
+ metrics.add(new Metric("content.proton.documentdb.notready.lid_space.lid_bloat_factor.average"));
+ metrics.add(new Metric("content.proton.documentdb.removed.lid_space.lid_bloat_factor.average"));
+ metrics.add(new Metric("content.proton.documentdb.ready.lid_space.lid_fragmentation_factor.average"));
+ metrics.add(new Metric("content.proton.documentdb.notready.lid_space.lid_fragmentation_factor.average"));
+ metrics.add(new Metric("content.proton.documentdb.removed.lid_space.lid_fragmentation_factor.average"));
+
+ // resource usage
+ metrics.add(new Metric("content.proton.resource_usage.disk.average"));
+ metrics.add(new Metric("content.proton.resource_usage.memory.average"));
+ metrics.add(new Metric("content.proton.resource_usage.feeding_blocked.last"));
+ metrics.add(new Metric("content.proton.documentdb.attribute.resource_usage.enum_store.average"));
+ metrics.add(new Metric("content.proton.documentdb.attribute.resource_usage.multi_value.average"));
+ metrics.add(new Metric("content.proton.documentdb.attribute.resource_usage.feeding_blocked.last"));
+
+ // transaction log
+ metrics.add(new Metric("content.proton.transactionlog.entries.average"));
+
+ // document store
+ metrics.add(new Metric("content.proton.documentdb.ready.document_store.disk_usage.average"));
+ metrics.add(new Metric("content.proton.documentdb.ready.document_store.disk_bloat.average"));
+ metrics.add(new Metric("content.proton.documentdb.ready.document_store.max_bucket_spread.average"));
+ metrics.add(new Metric("content.proton.documentdb.notready.document_store.disk_usage.average"));
+ metrics.add(new Metric("content.proton.documentdb.notready.document_store.disk_bloat.average"));
+ metrics.add(new Metric("content.proton.documentdb.notready.document_store.max_bucket_spread.average"));
+ metrics.add(new Metric("content.proton.documentdb.removed.document_store.disk_usage.average"));
+ metrics.add(new Metric("content.proton.documentdb.removed.document_store.disk_bloat.average"));
+ metrics.add(new Metric("content.proton.documentdb.removed.document_store.max_bucket_spread.average"));
+
+
+ //Storage
+ metrics.add(new Metric("vds.memfilepersistence.cache.files.average"));
+ metrics.add(new Metric("vds.memfilepersistence.cache.body.average"));
+ metrics.add(new Metric("vds.memfilepersistence.cache.header.average"));
+ metrics.add(new Metric("vds.memfilepersistence.cache.meta.average"));
+ metrics.add(new Metric("vds.visitor.allthreads.queuesize.count.average"));
+ metrics.add(new Metric("vds.visitor.allthreads.completed.sum.average"));
+ metrics.add(new Metric("vds.visitor.allthreads.created.sum.rate","visit"));
+
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.put.sum.latency.average"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.remove.sum.latency.average"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.get.sum.latency.average"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.update.sum.latency.average"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.splitbuckets.count.rate"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.joinbuckets.count.rate"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.setbucketstates.count.rate"));
+
+ metrics.add(new Metric("vds.filestor.spi.put.success.average"));
+ metrics.add(new Metric("vds.filestor.spi.remove.success.average"));
+ metrics.add(new Metric("vds.filestor.spi.update.success.average"));
+ metrics.add(new Metric("vds.filestor.spi.get.success.average"));
+ metrics.add(new Metric("vds.filestor.spi.iterate.success.average"));
+ metrics.add(new Metric("vds.filestor.spi.put.success.rate"));
+ metrics.add(new Metric("vds.filestor.spi.remove.success.rate"));
+ metrics.add(new Metric("vds.filestor.spi.update.success.rate"));
+ metrics.add(new Metric("vds.filestor.spi.get.success.rate"));
+ metrics.add(new Metric("vds.filestor.spi.iterate.success.rate"));
+
+
+ //Distributor
+ metrics.add(new Metric("vds.visitor.sum.latency.average"));
+ metrics.add(new Metric("vds.visitor.sum.failed.rate"));
+ metrics.add(new Metric("vds.idealstate.buckets_rechecking.average"));
+ metrics.add(new Metric("vds.idealstate.idealstate_diff.average"));
+ metrics.add(new Metric("vds.idealstate.buckets_toofewcopies.average"));
+ metrics.add(new Metric("vds.idealstate.buckets_toomanycopies.average"));
+ metrics.add(new Metric("vds.idealstate.buckets.average"));
+ metrics.add(new Metric("vds.idealstate.buckets_notrusted.average"));
+
+
+ metrics.add(new Metric("vds.distributor.puts.sum.latency.average"));
+ metrics.add(new Metric("vds.distributor.puts.sum.ok.rate"));
+ metrics.add(new Metric("vds.distributor.puts.sum.failures.total.rate"));
+ metrics.add(new Metric("vds.distributor.removes.sum.latency.average"));
+ metrics.add(new Metric("vds.distributor.removes.sum.ok.rate"));
+ metrics.add(new Metric("vds.distributor.removes.sum.failures.total.rate"));
+ metrics.add(new Metric("vds.distributor.updates.sum.latency.average"));
+ metrics.add(new Metric("vds.distributor.updates.sum.ok.rate"));
+ metrics.add(new Metric("vds.distributor.updates.sum.failures.total.rate"));
+ metrics.add(new Metric("vds.distributor.removelocations.sum.latency.average"));
+ metrics.add(new Metric("vds.distributor.removelocations.sum.ok.rate"));
+ metrics.add(new Metric("vds.distributor.removelocations.sum.failures.total.rate"));
+ metrics.add(new Metric("vds.distributor.gets.sum.latency.average"));
+ metrics.add(new Metric("vds.distributor.gets.sum.ok.rate"));
+ metrics.add(new Metric("vds.distributor.gets.sum.failures.total.rate"));
+ metrics.add(new Metric("vds.distributor.docsstored.average"));
+ metrics.add(new Metric("vds.distributor.bytesstored.average"));
+ metrics.add(new Metric("vds.visitor.sum.latency.average"));
+ metrics.add(new Metric("vds.visitor.sum.failed.rate"));
+
+ // Cluster Controller
+
+ metrics.add(new Metric("cluster-controller.down.count.last"));
+ metrics.add(new Metric("cluster-controller.initializing.count.last"));
+ metrics.add(new Metric("cluster-controller.maintenance.count.last"));
+ metrics.add(new Metric("cluster-controller.retired.count.last"));
+ metrics.add(new Metric("cluster-controller.stopping.count.last"));
+ metrics.add(new Metric("cluster-controller.up.count.last"));
+ metrics.add(new Metric("cluster-controller.cluster-state-change.count", "content.cluster-controller.cluster-state-change.count"));
+
+ metrics.add(new Metric("cluster-controller.is-master.last"));
+ // TODO(hakon): Update this name once persistent "count" metrics has been implemented.
+ // DO NOT RELY ON THIS METRIC YET.
+ metrics.add(new Metric("cluster-controller.node-event.count"));
+
+ //Errors from qrserver
+ metrics.add(new Metric("error.timeout.rate","error.timeout"));
+ metrics.add(new Metric("error.backends_oos.rate","error.backends_oos"));
+ metrics.add(new Metric("error.plugin_failure.rate","error.plugin_failure"));
+ metrics.add(new Metric("error.backend_communication_error.rate","error.backend_communication_error"));
+ metrics.add(new Metric("error.empty_document_summaries.rate","error.empty_document_summaries"));
+ metrics.add(new Metric("error.invalid_query_parameter.rate","error.invalid_query_parameter"));
+ metrics.add(new Metric("error.internal_server_error.rate", "error.internal_server_error"));
+ metrics.add(new Metric("error.misconfigured_server.rate","error.misconfigured_server"));
+ metrics.add(new Metric("error.invalid_query_transformation.rate","error.invalid_query_transformation"));
+ metrics.add(new Metric("error.result_with_errors.rate","error.result_with_errors"));
+ metrics.add(new Metric("error.unspecified.rate","error.unspecified"));
+ metrics.add(new Metric("error.unhandled_exception.rate","error.unhandled_exception"));
+ metrics.add(new Metric("http.status.1xx.rate"));
+ metrics.add(new Metric("http.status.2xx.rate"));
+ metrics.add(new Metric("http.status.3xx.rate"));
+ metrics.add(new Metric("http.status.4xx.rate"));
+ metrics.add(new Metric("http.status.5xx.rate"));
+
+
+ // container
+ metrics.add(new Metric("serverRejectedRequests.rate"));
+ metrics.add(new Metric("serverRejectedRequests.count"));
+
+ metrics.add(new Metric("serverThreadPoolSize.average"));
+ metrics.add(new Metric("serverThreadPoolSize.min"));
+ metrics.add(new Metric("serverThreadPoolSize.max"));
+ metrics.add(new Metric("serverThreadPoolSize.rate"));
+ metrics.add(new Metric("serverThreadPoolSize.count"));
+ metrics.add(new Metric("serverThreadPoolSize.last"));
+
+ metrics.add(new Metric("serverActiveThreads.average"));
+ metrics.add(new Metric("serverActiveThreads.min"));
+ metrics.add(new Metric("serverActiveThreads.max"));
+ metrics.add(new Metric("serverActiveThreads.rate"));
+ metrics.add(new Metric("serverActiveThreads.count"));
+ metrics.add(new Metric("serverActiveThreads.last"));
+
+ metrics.add(new Metric("httpapi_latency.average"));
+ metrics.add(new Metric("httpapi_pending.average"));
+ metrics.add(new Metric("httpapi_num_operations.rate"));
+ metrics.add(new Metric("httpapi_num_updates.rate"));
+ metrics.add(new Metric("httpapi_num_removes.rate"));
+ metrics.add(new Metric("httpapi_num_puts.rate"));
+ metrics.add(new Metric("httpapi_succeeded.rate"));
+ metrics.add(new Metric("httpapi_failed.rate"));
+
+
+ // Config server
+ metrics.add(new Metric("configserver.requests.count", "configserver.requests"));
+ metrics.add(new Metric("configserver.failedRequests.count", "configserver.failedRequests"));
+ metrics.add(new Metric("configserver.latency.average", "configserver.latency"));
+ metrics.add(new Metric("configserver.cacheConfigElems.last", "configserver.cacheConfigElems"));
+ metrics.add(new Metric("configserver.cacheChecksumElems.last", "configserver.cacheChecksumElems"));
+ metrics.add(new Metric("configserver.hosts.last", "configserver.hosts"));
+ metrics.add(new Metric("configserver.delayedResponses.count", "configserver.delayedResponses"));
+ metrics.add(new Metric("configserver.sessionChangeErrors.count", "configserver.sessionChangeErrors"));
+
+
+ Map<String, Metric> metricMap = new LinkedHashMap<>();
+ for (Metric metric : metrics) {
+ metricMap.put(metric.getName(), metric);
+ }
+
+ return new MetricsConsumer("yamas", metricMap);
+ }
+
+ // Common metrics for ymon and yamas. For ymon metric names needs to be less then 19 characters long
+ private List<Metric> commonMetrics(){
+ List<Metric> metrics = new ArrayList<>();
+
+ //Searchnode
+ metrics.add(new Metric("proton.numstoreddocs.last", "documents_total"));
+ metrics.add(new Metric("proton.numindexeddocs.last", "documents_ready"));
+ metrics.add(new Metric("proton.numactivedocs.last", "documents_active"));
+ metrics.add(new Metric("proton.numremoveddocs.last", "documents_removed"));
+
+ metrics.add(new Metric("proton.docsinmemory.last", "documents_inmemory"));
+ metrics.add(new Metric("proton.diskusage.last", "diskusage"));
+ metrics.add(new Metric("proton.memoryusage.max", "content.proton.memoryusage.max"));
+ metrics.add(new Metric("proton.transport.query.count.rate", "query_requests"));
+ metrics.add(new Metric("proton.transport.docsum.docs.rate", "document_requests"));
+ metrics.add(new Metric("proton.transport.docsum.latency.average", "content.proton.transport.docsum.latency.average"));
+ metrics.add(new Metric("proton.transport.query.latency.average", "query_latency"));
+
+ //Docproc - per chain
+ metrics.add(new Metric("documents_processed.rate", "documents_processed"));
+
+ //Qrserver
+ metrics.add(new Metric("peak_qps.average", "peak_qps"));
+ metrics.add(new Metric("search_connections.average", "search_connections"));
+ metrics.add(new Metric("active_queries.average", "active_queries"));
+ metrics.add(new Metric("queries.rate", "queries"));
+ metrics.add(new Metric("query_latency.average", "mean_query_latency"));
+ metrics.add(new Metric("query_latency.max", "max_query_latency"));
+ metrics.add(new Metric("query_latency.95percentile", "95p_query_latency"));
+ metrics.add(new Metric("query_latency.99percentile", "99p_query_latency"));
+ metrics.add(new Metric("failed_queries.rate", "failed_queries"));
+ metrics.add(new Metric("hits_per_query.average", "hits_per_query"));
+ metrics.add(new Metric("empty_results.rate", "empty_results"));
+ metrics.add(new Metric("requestsOverQuota.rate"));
+ metrics.add(new Metric("requestsOverQuota.count"));
+
+ //Storage
+ metrics.add(new Metric("vds.datastored.alldisks.docs.average","docs"));
+ metrics.add(new Metric("vds.datastored.alldisks.bytes.average","bytes"));
+ metrics.add(new Metric("vds.visitor.allthreads.averagevisitorlifetime.sum.average","visitorlifetime"));
+ metrics.add(new Metric("vds.visitor.allthreads.averagequeuewait.sum.average","visitorqueuewait"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.put.sum.count.rate","put"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.remove.sum.count.rate","remove"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.get.sum.count.rate","get"));
+ metrics.add(new Metric("vds.filestor.alldisks.allthreads.update.sum.count.rate","update"));
+ metrics.add(new Metric("vds.filestor.alldisks.queuesize.average","diskqueuesize"));
+ metrics.add(new Metric("vds.filestor.alldisks.averagequeuewait.sum.average","diskqueuewait"));
+
+
+ //Distributor
+ metrics.add(new Metric("vds.idealstate.delete_bucket.done_ok.rate","deleteok"));
+ metrics.add(new Metric("vds.idealstate.delete_bucket.done_failed.rate","deletefailed"));
+ metrics.add(new Metric("vds.idealstate.delete_bucket.pending.average","deletepending"));
+ metrics.add(new Metric("vds.idealstate.merge_bucket.done_ok.rate","mergeok"));
+ metrics.add(new Metric("vds.idealstate.merge_bucket.done_failed.rate","mergefailed"));
+ metrics.add(new Metric("vds.idealstate.merge_bucket.pending.average","mergepending"));
+ metrics.add(new Metric("vds.idealstate.split_bucket.done_ok.rate","splitok"));
+ metrics.add(new Metric("vds.idealstate.split_bucket.done_failed.rate","splitfailed"));
+ metrics.add(new Metric("vds.idealstate.split_bucket.pending.average","splitpending"));
+ metrics.add(new Metric("vds.idealstate.join_bucket.done_ok.rate","joinok"));
+ metrics.add(new Metric("vds.idealstate.join_bucket.done_failed.rate","joinfailed"));
+ metrics.add(new Metric("vds.idealstate.join_bucket.pending.average","joinpending"));
+
+ return metrics;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/FileDistributionOptions.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/FileDistributionOptions.java
new file mode 100644
index 00000000000..a5115d6c358
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/FileDistributionOptions.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.binaryprefix.BinaryPrefix;
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+import com.yahoo.cloud.config.filedistribution.FiledistributorConfig;
+
+/**
+ * Options for controlling the behavior of the file distribution services.
+ * @author tonytv
+ */
+public class FileDistributionOptions implements FiledistributorConfig.Producer {
+ public static FileDistributionOptions defaultOptions() {
+ return new FileDistributionOptions();
+ }
+
+ private FileDistributionOptions() {}
+
+ private BinaryScaledAmount uploadbitrate = new BinaryScaledAmount();
+ private BinaryScaledAmount downloadbitrate = new BinaryScaledAmount();
+
+ //Called through reflection
+ public void downloadbitrate(BinaryScaledAmount amount) {
+ ensureNonNegative(amount);
+ downloadbitrate = amount;
+ }
+
+ //Called through reflection
+ public void uploadbitrate(BinaryScaledAmount amount) {
+ ensureNonNegative(amount);
+ uploadbitrate = amount;
+ }
+
+ private void ensureNonNegative(BinaryScaledAmount amount) {
+ if (amount.amount < 0)
+ throw new IllegalArgumentException("Expected non-negative number, got " + amount.amount);
+ }
+
+ private int byteRate(BinaryScaledAmount bitRate) {
+ BinaryScaledAmount byteRate = bitRate.divide(8);
+ return (int)byteRate.as(BinaryPrefix.unit);
+ }
+
+ @Override
+ public void getConfig(FiledistributorConfig.Builder builder) {
+ builder.maxuploadspeed((double)byteRate(uploadbitrate));
+ builder.maxdownloadspeed((double)byteRate(downloadbitrate));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/Logserver.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/Logserver.java
new file mode 100644
index 00000000000..12341297ebb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/Logserver.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+
+/**
+ * Represents the Logserver. There is exactly one logserver in a Vespa
+ * system.
+ *
+ * @author gjoranv
+ */
+public class Logserver extends AbstractService {
+ private static final long serialVersionUID = 1L;
+ private static final String logArchiveDir = "$ROOT/logs/vespa/logarchive";
+
+ public Logserver(AbstractConfigProducer parent) {
+ super(parent, "logserver");
+ portsMeta.on(0).tag("unused");
+ portsMeta.on(1).tag("logtp");
+ portsMeta.on(2).tag("logtp").tag("telnet").tag("last-errors-holder");
+ portsMeta.on(3).tag("logtp").tag("telnet").tag("replicator");
+ setProp("clustertype", "admin");
+ setProp("clustername", "admin");
+ monitorService();
+ }
+
+ /**
+ * @return the startup command for the logserver
+ */
+ public String getStartupCommand() {
+ return "exec $ROOT/bin/logserver-start " + getMyJVMArgs() + " " + getJvmArgs();
+ }
+
+ /**
+ * @return the jvm args to be used by the logserver.
+ */
+ private String getMyJVMArgs() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("-Dlogserver.http.port=").append(getRelativePort(0));
+ sb.append(" ");
+ sb.append("-Dlogserver.listenport=").append(getRelativePort(1));
+ sb.append(" ");
+ sb.append("-Dlogserver.last-errors-holder.port=").append(getRelativePort(2));
+ sb.append(" ");
+ sb.append("-Dlogserver.replicator.port=").append(getRelativePort(3));
+ sb.append(" ");
+ sb.append("-Dlogserver.logarchive.dir=" + logArchiveDir);
+ return sb.toString();
+ }
+
+ /**
+ * Returns the desired base port for this service.
+ */
+ public int getWantedPort() {
+ return 19080;
+ }
+
+ /**
+ * The desired base port is the only allowed base port.
+ *
+ * @return 'true' always
+ */
+ public boolean requiresWantedPort() {
+ return true;
+ }
+
+ /**
+ * @return the number of ports needed by the logserver.
+ */
+ public int getPortCount() {
+ return 4;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/Metric.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/Metric.java
new file mode 100644
index 00000000000..e1056f954dc
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/Metric.java
@@ -0,0 +1,99 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Helper class to model a metric.
+ *
+ * @author trygve
+ */
+public class Metric {
+
+ private final String name;
+ private String outputName;
+ private final Map<String, String> dimensions = new HashMap<>();
+ private final String description;
+
+ /**
+ * @param name The metric name
+ * @param outputName The name of the metric in yamas
+ * @param description The description of this metric
+ */
+ public Metric(String name, String outputName, String description) {
+ this.name = name;
+ this.outputName = outputName;
+ this.description = description;
+ }
+
+ /**
+ * Creates a metric with empty dimensions and consumers containing the default consumer
+ *
+ * @param name the metric name
+ * @param outputName name tp be used in yamas
+ */
+ public Metric(String name, String outputName) {
+ this(name, outputName, "");
+ }
+
+ /**
+ * Creates a metric with same outputname as metricname and empty dimensions and consumers containing the default consumer and
+ *
+ * @param name The name of the metric, same name used for outputname
+ */
+ public Metric(String name) {
+ this(name, name);
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+
+ public String getOutputName() {
+ return outputName;
+ }
+
+ public void setOutputName(String outputName) {
+ this.outputName = outputName;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Map<String, String> getDimensions() {
+ return dimensions;
+ }
+
+ @Override
+ public String toString() {
+ return "Metric{" +
+ "name='" + name + '\'' +
+ ", outputName='" + outputName + '\'' +
+ ", dimensions=" + dimensions +
+ '}';
+ }
+
+
+ /**
+ * Two metrics are considered equal if they have the same name.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Metric metric = (Metric) o;
+
+ return name.equals(metric.name);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/MetricsConsumer.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/MetricsConsumer.java
new file mode 100644
index 00000000000..0bed49f38ac
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/MetricsConsumer.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import java.util.Map;
+
+/**
+ * Represents an arbitrary metric consumer
+ *
+ * @author trygve
+ */
+public class MetricsConsumer {
+ private final String consumer;
+ private final Map<String, Metric> metrics;
+
+ /**
+ * @param consumer The consumer
+ * @param metrics The metrics for the the consumer
+ */
+ public MetricsConsumer(String consumer, Map<String, Metric> metrics) {
+ this.consumer = consumer;
+ this.metrics = metrics;
+ }
+
+ public String getConsumer() {
+ return consumer;
+ }
+
+ /**
+ * @return Map of metric with metric name as key
+ */
+ public Map<String, Metric> getMetrics() {
+ return metrics;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/ModelConfigProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/ModelConfigProvider.java
new file mode 100644
index 00000000000..1e9b485a939
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/ModelConfigProvider.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * A config provider for the model config. The ModelConfig is a common config and produced by {@link ApplicationConfigProducerRoot} , this config
+ * producer exists to make the admin/model config id exist for legacy reasons.
+ * @author <a href="mailto:musum@yahoo-inc.com">musum</a>
+ * @author gjoranv
+ * @since 5.0.8
+ */
+public class ModelConfigProvider extends AbstractConfigProducer {
+
+ public ModelConfigProvider(AbstractConfigProducer<?> parent) {
+ super(parent, "model");
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/MonitoringSystem.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/MonitoringSystem.java
new file mode 100644
index 00000000000..de95c9a8d9a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/MonitoringSystem.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+/**
+ * Interface for different monitoring services
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ */
+public interface MonitoringSystem {
+ /**
+ * @return Snapshot interval in minutes
+ */
+ public Integer getInterval();
+
+ /**
+ * @return Snapshot interval in seconds.
+ */
+ public Integer getIntervalSeconds();
+
+ /**
+ * @return the monitoring cluster name
+ */
+ public String getClustername();
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/Slobrok.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/Slobrok.java
new file mode 100644
index 00000000000..a2b7c7a5532
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/Slobrok.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+
+/**
+ * Represents a Slobrok service.
+ *
+ * @author gjoranv
+ */
+public class Slobrok extends AbstractService {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param parent The parent ConfigProducer.
+ * @param index unique index for all slobroks
+ */
+ public Slobrok(AbstractConfigProducer parent, int index) {
+ super(parent, "slobrok." + index);
+ portsMeta.on(0).tag("rpc").tag("admin").tag("status");
+ portsMeta.on(1).tag("http").tag("state");
+ setProp("index", index);
+ setProp("clustertype", "slobrok");
+ setProp("clustername", "admin");
+ monitorService();
+ }
+
+ @Override
+ public boolean requiresConsecutivePorts() {
+ return false;
+ }
+
+ @Override
+ public int getWantedPort() {
+ if (getId() == 1) {
+ return 19099;
+ } else {
+ return 0;
+ }
+ }
+
+ public String getStartupCommand() {
+ return "exec $ROOT/bin/slobrok -p " + getPort() +
+ " -s " + getStatePort() +
+ " -c " + getConfigId();
+ }
+
+ /**
+ * @return The number of ports needed by the slobrok.
+ */
+ public int getPortCount() {
+ return 2;
+ }
+
+ /**
+ * @return The port on which this slobrok should respond, as a String.
+ */
+ public String getPort() {
+ return String.valueOf(getRelativePort(0));
+ }
+
+ /**
+ * @return The port on which the state server should respond
+ */
+ public String getStatePort() {
+ return String.valueOf(getRelativePort(1));
+ }
+
+ /**
+ * @return The connection spec to this Slobrok
+ */
+ public String getConnectionSpec() {
+ return "tcp/" + getHostName() + ":" + getPort();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/Yamas.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/Yamas.java
new file mode 100644
index 00000000000..43775fe0dec
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/Yamas.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Properties for yamas monitoring service
+ *
+ * @author musum
+ * @since 5.1.20
+ */
+public class Yamas extends AbstractMonitoringSystem implements Serializable {
+ public Yamas(String clustername, Integer interval) {
+ super(clustername, interval);
+ }
+}
+
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/ZooKeepersConfigProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/ZooKeepersConfigProvider.java
new file mode 100644
index 00000000000..6c8b486c8a6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/ZooKeepersConfigProvider.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.collections.CollectionUtil;
+import com.yahoo.cloud.config.ZookeepersConfig;
+import com.yahoo.config.model.api.ConfigServerSpec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author tonytv
+ */
+public class ZooKeepersConfigProvider implements ZookeepersConfig.Producer {
+
+ public static final int zkPort = Integer.getInteger("zk_port", 2181);
+
+ private final List<Configserver> configServers;
+
+ public ZooKeepersConfigProvider(List<Configserver> configServers) {
+ if (configServers == null) {
+ configServers = new ArrayList<>();
+ }
+ this.configServers = configServers;
+ }
+
+ // format for each element: hostname:port
+ public List<String> getZooKeepers() {
+ List<String> servers = new ArrayList<>();
+ for (Configserver server : configServers) {
+ ConfigServerSpec serverSpec = server.getConfigServerSpec();
+ servers.add(serverSpec.getHostName() + ":" + serverSpec.getZooKeeperPort());
+
+ }
+ return servers;
+ }
+
+ // format: hostname1:port2,hostname2:port2,...
+ public String getZooKeepersConnectionSpec() {
+ return CollectionUtil.mkString(getZooKeepers(), ",");
+ }
+
+ @Override
+ public void getConfig(ZookeepersConfig.Builder builder) {
+ builder.zookeeperserverlist(getZooKeepersConnectionSpec());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerCluster.java
new file mode 100644
index 00000000000..b82f58fbb40
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerCluster.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin.clustercontroller;
+
+import com.google.common.base.Joiner;
+import com.yahoo.cloud.config.ZookeeperServerConfig;
+import com.yahoo.cloud.config.ZookeepersConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.admin.Configserver;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Used if clustercontroller is run standalone (not as part of the config server ZooKeeper cluster)
+ * to provide common configs to container components.
+ *
+ * @author lulf
+ * @since 5.6
+ */
+public class ClusterControllerCluster extends AbstractConfigProducer<ContainerCluster> implements ZookeeperServerConfig.Producer, ZookeepersConfig.Producer {
+
+ private static final int ZK_CLIENT_PORT = 2181;
+ private ContainerCluster containerCluster = null;
+
+ public ClusterControllerCluster(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ }
+
+ @Override
+ public void getConfig(ZookeeperServerConfig.Builder builder) {
+ builder.clientPort(ZK_CLIENT_PORT);
+ for (Container c : containerCluster.getContainers()) {
+ ClusterControllerContainer container = (ClusterControllerContainer) c;
+ ZookeeperServerConfig.Server.Builder serverBuilder = new ZookeeperServerConfig.Server.Builder();
+ serverBuilder.hostname(container.getHostName());
+ serverBuilder.id(container.getIndex());
+ builder.server(serverBuilder);
+ }
+ }
+
+ @Override
+ public void getConfig(ZookeepersConfig.Builder builder) {
+ final Collection<String> controllerHosts = new ArrayList<>();
+ for (Container container : containerCluster.getContainers()) {
+ controllerHosts.add(container.getHostName() + ":" + ZK_CLIENT_PORT);
+ }
+ builder.zookeeperserverlist(Joiner.on(",").join(controllerHosts));
+ }
+
+ @Override
+ protected void addChild(ContainerCluster cluster) {
+ super.addChild(cluster);
+ this.containerCluster = cluster;
+ }
+
+ @Override
+ public void validate() {
+ assert(containerCluster != null);
+ for (Container c1 : containerCluster.getContainers()) {
+ assert(c1 instanceof ClusterControllerContainer);
+ for (Service service : c1.getHostResource().getServices()) {
+ if (service instanceof Configserver) {
+ throw new IllegalArgumentException("Error validating cluster controller cluster: cluster controller '" + c1.getConfigId() + "' is set to run on the same host as a configserver");
+ }
+ }
+ for (Container c2 : containerCluster.getContainers()) {
+ if (c1 != c2 && c1.getHostName().equals(c2.getHostName())) {
+ throw new IllegalArgumentException("Error validating cluster controller cluster: cluster controllers '" + c1.getConfigId() + "' and '" + c2.getConfigId() + "' share the same host");
+ }
+ }
+ }
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerComponent.java
new file mode 100644
index 00000000000..8ec1cecf47a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerComponent.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin.clustercontroller;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+
+/**
+ * Sets up a simple component to keep the state of the cluster controller, even when configuration changes.
+ */
+public class ClusterControllerComponent extends SimpleComponent
+{
+ public ClusterControllerComponent() {
+ super(new ComponentModel(new BundleInstantiationSpecification(
+ new ComponentSpecification("clustercontroller"),
+ new ComponentSpecification("com.yahoo.vespa.clustercontroller.apps.clustercontroller.ClusterController"),
+ new ComponentSpecification("clustercontroller-apps"))));
+ }
+} \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerConfigurer.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerConfigurer.java
new file mode 100644
index 00000000000..e3e79a57261
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerConfigurer.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin.clustercontroller;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+/**
+ * Model serving class. Wraps fleet controller config serving.
+ */
+public class ClusterControllerConfigurer extends SimpleComponent implements StorDistributionConfig.Producer,
+ FleetcontrollerConfig.Producer
+{
+ private final ContentCluster cluster;
+ private final int index;
+ private final int nodeCount;
+
+ public ClusterControllerConfigurer(ContentCluster cluster, int index, int nodeCount) {
+ super(new ComponentModel(new BundleInstantiationSpecification(
+ new ComponentSpecification("clustercontroller" + "-" + cluster.getName() + "-configurer"),
+ new ComponentSpecification("com.yahoo.vespa.clustercontroller.apps.clustercontroller.ClusterControllerClusterConfigurer"),
+ new ComponentSpecification("clustercontroller-apps"))));
+ this.cluster = cluster;
+ this.index = index;
+ this.nodeCount = nodeCount;
+ }
+
+ @Override
+ public void getConfig(StorDistributionConfig.Builder builder) {
+ cluster.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(FleetcontrollerConfig.Builder builder) {
+ cluster.getConfig(builder);
+ cluster.getClusterControllerConfig().getConfig(builder);
+ builder.index(index);
+ builder.fleet_controller_count(nodeCount);
+ builder.http_port(0);
+ builder.rpc_port(0);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java
new file mode 100644
index 00000000000..1290b0b22d6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/ClusterControllerContainer.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin.clustercontroller;
+
+import com.yahoo.cloud.config.ZookeeperServerConfig;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.BundlesConfig;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.log.LogLevel;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import static com.yahoo.vespa.defaults.Defaults.getDefaults;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.component.AccessLogComponent;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.Handler;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Extends the container producer to allow us to override ports.
+ */
+@RestartConfigs({FleetcontrollerConfig.class, ZookeeperServerConfig.class})
+public class ClusterControllerContainer extends Container implements BundlesConfig.Producer, ZookeeperServerConfig.Producer {
+ private static final ComponentSpecification CLUSTERCONTROLLER_BUNDLE = new ComponentSpecification("clustercontroller-apps");
+ private static final ComponentSpecification ZKFACADE_BUNDLE = new ComponentSpecification("zkfacade");
+ private final int index;
+
+ private final Set<String> bundles = new TreeSet<>();
+
+ public ClusterControllerContainer(AbstractConfigProducer parent, int index, boolean runStandaloneZooKeeper) {
+ super(parent, "" + index);
+ this.index = index;
+ addHandler(
+ new Handler(new ComponentModel(new BundleInstantiationSpecification(
+ new ComponentSpecification("clustercontroller-status"),
+ new ComponentSpecification("com.yahoo.vespa.clustercontroller.apps.clustercontroller.StatusHandler"),
+ CLUSTERCONTROLLER_BUNDLE))), "clustercontroller-status/*"
+ );
+ addHandler(
+ new Handler(new ComponentModel(new BundleInstantiationSpecification(
+ new ComponentSpecification("clustercontroller-state-restapi-v2"),
+ new ComponentSpecification("com.yahoo.vespa.clustercontroller.apps.clustercontroller.StateRestApiV2Handler"),
+ CLUSTERCONTROLLER_BUNDLE))), "cluster/v2/*"
+ );
+ if (runStandaloneZooKeeper) {
+ addComponent(new Component<>(new ComponentModel(new BundleInstantiationSpecification(
+ new ComponentSpecification("clustercontroller-zkrunner"),
+ new ComponentSpecification("com.yahoo.vespa.zookeeper.ZooKeeperServer"), ZKFACADE_BUNDLE))));
+ addComponent(new Component<>(new ComponentModel(new BundleInstantiationSpecification(
+ new ComponentSpecification("clustercontroller-zkprovider"),
+ new ComponentSpecification("com.yahoo.vespa.clustercontroller.apps.clustercontroller.StandaloneZooKeeperProvider"), CLUSTERCONTROLLER_BUNDLE))));
+ } else {
+ addComponent(new Component<>(new ComponentModel(new BundleInstantiationSpecification(
+ new ComponentSpecification("clustercontroller-zkprovider"),
+ new ComponentSpecification("com.yahoo.vespa.clustercontroller.apps.clustercontroller.DummyZooKeeperProvider"), CLUSTERCONTROLLER_BUNDLE))));
+ }
+ addBundle("file:" + getDefaults().underVespaHome("lib/jars/clustercontroller-apps-jar-with-dependencies.jar"));
+ addBundle("file:" + getDefaults().underVespaHome("lib/jars/clustercontroller-apputil-jar-with-dependencies.jar"));
+ addBundle("file:" + getDefaults().underVespaHome("lib/jars/clustercontroller-core-jar-with-dependencies.jar"));
+ addBundle("file:" + getDefaults().underVespaHome("lib/jars/clustercontroller-utils-jar-with-dependencies.jar"));
+ addBundle("file:" + getDefaults().underVespaHome("lib/jars/zkfacade-jar-with-dependencies.jar"));
+
+ log.log(LogLevel.DEBUG, "Adding access log for cluster controller ...");
+ addComponent(new AccessLogComponent(AccessLogComponent.AccessLogType.queryAccessLog, "controller"));
+ }
+
+ @Override
+ public int getWantedPort() {
+ return 19050;
+ }
+
+ @Override
+ public boolean requiresWantedPort() {
+ return index == 0;
+ }
+
+ @Override
+ public String getServiceType() {
+ return "container-clustercontroller";
+ }
+
+ private void addHandler(Handler h, String binding) {
+ h.addServerBindings("http://*/" + binding,
+ "https://*/" + binding);
+ super.addHandler(h);
+ }
+
+ public void addBundle(String bundlePath) {
+ bundles.add(bundlePath);
+ }
+
+ @Override
+ public void getConfig(BundlesConfig.Builder builder) {
+ for (String bundle : bundles) {
+ builder.bundle(bundle);
+ }
+ }
+
+ @Override
+ public void getConfig(ZookeeperServerConfig.Builder builder) {
+ builder.myid(index);
+ }
+
+ int getIndex() {
+ return index;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/package-info.java
new file mode 100644
index 00000000000..bd3556cc605
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/clustercontroller/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.admin.clustercontroller;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/package-info.java
new file mode 100644
index 00000000000..e3c7221f7b7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/package-info.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Provides the classes for the admin components of the Vespa config
+ * model.
+ *
+ * The {@link com.yahoo.vespa.model.admin.Admin Admin} class is
+ * the natural starting point. It takes the admin part of the
+ * user-defined application specification as input argument to its
+ * constructor. The services given in the specification are
+ * instantiated, in addition to some mandatory services (e.g. {@link
+ * com.yahoo.vespa.model.admin.Slobrok Slobrok}) that will be created
+ * even if the user does not specify them.
+ */
+@ExportPackage
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ComponentValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ComponentValidator.java
new file mode 100644
index 00000000000..00553f681fe
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ComponentValidator.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.config.application.api.ComponentInfo;
+import com.yahoo.config.application.api.DeployLogger;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.logging.Level;
+import java.util.zip.ZipException;
+
+/**
+ * A validator for bundles. Uses BND library for some of the validation (not active yet)
+ *
+ * @author musum
+ * @since 2010-11-11
+ */
+public class ComponentValidator extends Validator {
+ private JarFile jarFile;
+
+ public ComponentValidator() {
+ }
+
+ public ComponentValidator(JarFile jarFile) {
+ this.jarFile = jarFile;
+ }
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ ApplicationPackage app = deployState.getApplicationPackage();
+ for (ComponentInfo info : app.getComponentsInfo(deployState.getProperties().vespaVersion())) {
+ try {
+ this.jarFile = new JarFile(app.getFileReference(Path.fromString(info.getPathRelativeToAppDir())));
+ } catch (ZipException e) {
+ throw new IllegalArgumentException("Error opening jar file '" + info.getPathRelativeToAppDir() +
+ "'. Please check that this is a valid jar file");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ try {
+ validateAll(deployState.getDeployLogger());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void validateAll(DeployLogger deployLogger) throws IOException {
+ validateOSGIHeaders(deployLogger);
+ }
+
+ public void validateOSGIHeaders(DeployLogger deployLogger) throws IOException {
+ Manifest mf = jarFile.getManifest();
+ if (mf == null) {
+ throw new IllegalArgumentException("Non-existing or invalid manifest in " + jarFile.getName());
+ }
+
+ // Check for required OSGI headers
+ Attributes attributes = mf.getMainAttributes();
+ HashSet<String> mfAttributes = new HashSet<>();
+ for (Object attributeSet : attributes.entrySet()) {
+ Map.Entry<Object, Object> e = (Map.Entry<Object, Object>) attributeSet;
+ mfAttributes.add(e.getKey().toString());
+ }
+ List<String> requiredOSGIHeaders = Arrays.asList(
+ "Bundle-ManifestVersion", "Bundle-Name", "Bundle-SymbolicName", "Bundle-Version");
+ for (String header : requiredOSGIHeaders) {
+ if (!mfAttributes.contains(header)) {
+ throw new IllegalArgumentException("Required OSGI header '" + header +
+ "' was not found in manifest in '" + jarFile.getName() + "'");
+ }
+ }
+
+ if (attributes.getValue("Bundle-Version").endsWith(".SNAPSHOT")) {
+ deployLogger.log(Level.WARNING, "Deploying snapshot bundle " + jarFile.getName() +
+ ".\nTo use this bundle, you must include the qualifier 'SNAPSHOT' in the version specification in services.xml.");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidator.java
new file mode 100644
index 00000000000..9b62d66bc07
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidator.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+
+import java.io.Reader;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Validates that deployment file (deployment.xml) has valid values (for now
+ * only global-service-id is validated)
+ *
+ * @author musum
+ */
+public class DeploymentFileValidator extends Validator {
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ Optional<Reader> deployment = deployState.getApplicationPackage().getDeployment();
+
+ if (deployment.isPresent()) {
+ Reader deploymentReader = deployment.get();
+ DeploymentSpec deploymentSpec = DeploymentSpec.fromXml(deploymentReader);
+ final Optional<String> globalServiceId = deploymentSpec.globalServiceId();
+ if (globalServiceId.isPresent()) {
+ Set<ContainerCluster> containerClusters = model.getRoot().configModelRepo().getModels(ContainerModel.class).stream().
+ map(ContainerModel::getCluster).filter(cc -> cc.getName().equals(globalServiceId.get())).collect(Collectors.toSet());
+ if (containerClusters.size() != 1) {
+ throw new IllegalArgumentException("global-service-id '" + globalServiceId.get() + "' specified in deployment.xml does not match any container cluster id");
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexes.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexes.java
new file mode 100644
index 00000000000..d9aa600a840
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexes.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.search.DocumentDatabase;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+
+import java.util.Map;
+
+/**
+ * match:prefix for indexed fields not supported
+ * @author vegardh
+ *
+ */
+public class NoPrefixForIndexes extends Validator {
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ for (AbstractSearchCluster cluster : model.getSearchClusters()) {
+ if (cluster instanceof IndexedSearchCluster) {
+ IndexedSearchCluster sc = (IndexedSearchCluster) cluster;
+ for (DocumentDatabase docDb : sc.getDocumentDbs()) {
+ DerivedConfiguration sdConfig = docDb.getDerivedConfiguration();
+ Search search = sdConfig.getSearch();
+ for (SDField field : search.allFieldsList()) {
+ if (field.doesIndexing()) {
+ //if (!field.getIndexTo().isEmpty() && !field.getIndexTo().contains(field.getName())) continue;
+ if (field.getMatching().getAlgorithm().equals(Matching.Algorithm.PREFIX)) {
+ failField(search, field);
+ }
+ for (Map.Entry<String, Index> e : field.getIndices().entrySet()) {
+ if (e.getValue().isPrefix()) {
+ failField(search, field);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void failField(Search search, SDField field) {
+ throw new IllegalArgumentException("For search '" + search.getName() + "', field '" + field.getName() +
+ "': match/index:prefix is not supported for indexes.");
+ }
+} \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RankSetupValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RankSetupValidator.java
new file mode 100644
index 00000000000..89a9d245de7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RankSetupValidator.java
@@ -0,0 +1,159 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.io.IOUtils;
+import com.yahoo.log.InvalidLogFormatException;
+import com.yahoo.log.LogMessage;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.system.ProcessExecuter;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.collections.Pair;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.search.IndexschemaConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.search.DocumentDatabase;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import com.yahoo.vespa.model.search.SearchCluster;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Level;
+
+/**
+ * Validate rank setup for all search clusters (rank-profiles, index-schema, attributes configs), validating done
+ * by running through the binary 'verify_ranksetup'
+ *
+ * @author vegardh
+ *
+ */
+public class RankSetupValidator extends Validator {
+ private final boolean force;
+
+ public RankSetupValidator(boolean force) {
+ this.force = force;
+ }
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ File cfgDir = makeTempConfigDir(deployState.getDeployLogger());
+ if (cfgDir == null) return;
+
+ for (AbstractSearchCluster cluster : model.getSearchClusters()) {
+ // Skipping rank expression checking for streaming clusters, not implemented yet
+ if (cluster.isRealtime()) {
+ IndexedSearchCluster sc = (IndexedSearchCluster) cluster;
+ String clusterDir = cfgDir.getAbsolutePath() + "/" + sc.getClusterName() + "/";
+ for (DocumentDatabase docDb : sc.getDocumentDbs()) {
+ String searchDir = clusterDir + docDb.getDerivedConfiguration().getSearch().getName() + "/";
+ writeConfigs(searchDir, docDb);
+ if (!validate("dir:" + searchDir, sc, docDb.getDerivedConfiguration().getSearch().getName(), deployState.getDeployLogger(), cfgDir)) {
+ return;
+ }
+ }
+ }
+ }
+ deleteTempDir(cfgDir);
+ }
+
+ private boolean validate(String configId, SearchCluster sc, String sdName, DeployLogger logger, File tempDir) {
+ try {
+ boolean ret = execValidate(configId, sc, sdName, logger);
+ if (!ret) {
+ // Give up, don't say same error msg repeatedly
+ deleteTempDir(tempDir);
+ }
+ return ret;
+ } catch (IllegalArgumentException e) {
+ deleteTempDir(tempDir);
+ throw e;
+ }
+ }
+
+ private void deleteTempDir(File dir) {
+ if (!IOUtils.recursiveDeleteDir(dir)) {
+ throw new RuntimeException("Failed deleting " + dir);
+ }
+ }
+
+ private void writeConfigs(String dir, AbstractConfigProducer producer) {
+ try {
+ RankProfilesConfig.Builder rpb = new RankProfilesConfig.Builder();
+ RankProfilesConfig.Producer rpProd = (RankProfilesConfig.Producer) producer;
+ rpProd.getConfig(rpb);
+ writeConfig(dir, "rank-profiles.cfg", new RankProfilesConfig(rpb));
+
+ IndexschemaConfig.Builder isB = new IndexschemaConfig.Builder();
+ IndexschemaConfig.Producer isProd = (IndexschemaConfig.Producer) producer;
+ isProd.getConfig(isB);
+ writeConfig(dir, "indexschema.cfg", new IndexschemaConfig(isB));
+
+ AttributesConfig.Builder acb = new AttributesConfig.Builder();
+ AttributesConfig.Producer acProd = (AttributesConfig.Producer) producer;
+ acProd.getConfig(acb);
+ writeConfig(dir, "attributes.cfg", new AttributesConfig(acb));
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private static void writeConfig(String dir, String configName, ConfigInstance config) throws IOException {
+ IOUtils.writeFile(dir + configName, StringUtilities.implodeMultiline(ConfigInstance.serialize(config)), false);
+ }
+
+ private boolean execValidate(String configId, SearchCluster sc, String sdName, DeployLogger logger) {
+ String job = "verify_ranksetup-bin " + configId;
+ ProcessExecuter executer = new ProcessExecuter();
+ try {
+ Pair<Integer, String> ret = executer.exec(job);
+ if (ret.getFirst() != 0) {
+ validateFail(ret.getSecond(), sc, sdName, logger);
+ }
+ } catch (IOException e) {
+ validateWarn(executer, e, logger);
+ return false;
+ }
+ return true;
+ }
+
+ @SuppressWarnings({"UnusedDeclaration"})
+ private void validateWarn(ProcessExecuter executer, Exception e, DeployLogger logger) {
+ String msg = "Unable to execute 'verify_ranksetup', validation of rank expressions will only take place when you start Vespa: " +
+ Exceptions.toMessageString(e);
+ logger.log(Level.WARNING, msg);
+ }
+
+ private void validateFail(String output, SearchCluster sc, String sdName, DeployLogger logger) {
+ String errMsg = "For search cluster '" + sc.getClusterName() + "', search definition '" + sdName + "': error in rank setup. Details:\n";
+ for (String line : output.split("\n")) {
+ // Remove debug lines from start script
+ if (line.startsWith("debug\t")) continue;
+ try {
+ LogMessage logmsg = LogMessage.parseNativeFormat(line);
+ errMsg = errMsg + logmsg.getLevel() + ": " + logmsg.getPayload() + "\n";
+ } catch (InvalidLogFormatException e) {
+ errMsg = errMsg + line + "\n";
+ }
+ }
+ if (force) {
+ logger.log(Level.WARNING, errMsg + "(Continuing because of force.)");
+ } else {
+ throw new IllegalArgumentException(errMsg);
+ }
+ }
+
+ private File makeTempConfigDir(DeployLogger deployLogger) {
+ String name = "/tmp/deploy_ranksetup_" + System.currentTimeMillis() + "/";
+ File tempDir = new File(name);
+ if (!tempDir.mkdir()) {
+ deployLogger.log(Level.WARNING, "Not able to create '" + name + "' when validating rank setup");
+ return null;
+ }
+ return tempDir;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RestartConfigs.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RestartConfigs.java
new file mode 100644
index 00000000000..1bf15d6dd4f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RestartConfigs.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.model.application.validation.change.ConfigValueChangeValidator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Services should use this annotation to list all consumed configurations which contain definitions flagged with restart.
+ * This annotation is required for the {@link ConfigValueChangeValidator}
+ * to detect config changes that will require restart of some services. The {@link com.yahoo.config.ConfigInstance}
+ * values are inherited; any configs annotated on a service will be inherited to all sub-classes of that service.
+ * These sub-classes can supplement with more ConfigInstances (in addition to the inherited one) with the annotation.
+ * This is different the inheritance that {@link java.lang.annotation.Inherited} provides, where sub-classes can either
+ * inherit or override the annotation from the super class.
+ *
+ * NOTE: This annotation is will only have effect on subclasses of {@link com.yahoo.vespa.model.Service}.
+ * Do not use this annotation on other types config producers.
+ *
+ * @author bjorncs
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface RestartConfigs {
+ Class<? extends ConfigInstance>[] value() default {};
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingSelectorValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingSelectorValidator.java
new file mode 100644
index 00000000000..ad79b1eed45
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingSelectorValidator.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.document.select.DocumentSelector;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+
+
+/**
+ * Validates routing selector for search and content clusters
+ *
+ */
+public class RoutingSelectorValidator extends Validator {
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ for (AbstractSearchCluster cluster : model.getSearchClusters()) {
+ if (cluster instanceof IndexedSearchCluster) {
+ IndexedSearchCluster sc = (IndexedSearchCluster) cluster;
+ String routingSelector = sc.getRoutingSelector();
+ if (routingSelector == null) continue;
+ try {
+ new DocumentSelector(routingSelector);
+ } catch (com.yahoo.document.select.parser.ParseException e) {
+ throw new IllegalArgumentException("Failed to parse routing selector for search cluster '" +
+ sc.getClusterName() + "'", e);
+ }
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingValidator.java
new file mode 100644
index 00000000000..6e94c39cc51
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/RoutingValidator.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.vespa.model.VespaModel;
+
+import java.util.List;
+
+/**
+ * Validates routing
+ *
+ */
+public class RoutingValidator extends Validator {
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ List<String> errors = model.getRouting().getErrors();
+ if (!errors.isEmpty()) {
+ StringBuilder msg = new StringBuilder();
+ msg.append("The routing specification contains ").append(errors.size()).append(" error(s):\n");
+ for (int i = 0, len = errors.size(); i < len; ++i) {
+ msg.append(i + 1).append(". ").append(errors.get(i)).append("\n");
+ }
+ throw new IllegalArgumentException(msg.toString());
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidator.java
new file mode 100644
index 00000000000..c03fb0617b8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidator.java
@@ -0,0 +1,86 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.search.SearchDefinition;
+
+import java.util.List;
+
+/**
+ * This Validator iterates through all search cluster in the given VespaModel to make sure that there are no custom
+ * structs defined in any of its search definitions.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SearchDataTypeValidator extends Validator {
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ List<AbstractSearchCluster> clusters = model.getSearchClusters();
+ for (AbstractSearchCluster cluster : clusters) {
+ if (cluster.isStreaming()) {
+ continue;
+ }
+ for (AbstractSearchCluster.SearchDefinitionSpec spec : cluster.getLocalSDS()) {
+ SDDocumentType docType = spec.getSearchDefinition().getSearch().getDocument();
+ if (docType == null) {
+ continue;
+ }
+ validateDocument(cluster, spec.getSearchDefinition(), docType);
+ }
+ }
+ }
+
+ private void validateDocument(AbstractSearchCluster cluster, SearchDefinition def, SDDocumentType doc) {
+ for (SDDocumentType child : doc.getTypes()) {
+ validateDocument(cluster, def, child);
+ }
+ for (Field field : doc.fieldSet()) {
+ DataType fieldType = field.getDataType();
+ disallowIndexingOfMaps(cluster, def, field);
+ if (!validateDataType(fieldType)) {
+ throw new IllegalArgumentException("Field type '" + fieldType.getName() + "' is illegal for search " +
+ "clusters (field '" + field.getName() + "' in definition '" +
+ def.getName() + "' for cluster '" + cluster.getClusterName() + "').");
+ }
+ }
+ }
+
+ private boolean validateDataType(DataType dataType) {
+ if (dataType instanceof ArrayDataType ||
+ dataType instanceof WeightedSetDataType)
+ {
+ return validateDataType(((CollectionDataType)dataType).getNestedType());
+ }
+ if (dataType instanceof StructDataType) {
+ return true; // Struct will work for summary TODO maybe check individual fields
+ }
+ if (dataType instanceof MapDataType) {
+ return true; // Maps will work for summary, see disallowIndexingOfMaps()
+ }
+ return dataType.equals(DataType.INT) ||
+ dataType.equals(DataType.FLOAT) ||
+ dataType.equals(DataType.STRING) ||
+ dataType.equals(DataType.RAW) ||
+ dataType.equals(DataType.LONG) ||
+ dataType.equals(DataType.DOUBLE) ||
+ dataType.equals(DataType.URI) ||
+ dataType.equals(DataType.BYTE) ||
+ dataType.equals(DataType.PREDICATE) ||
+ dataType.equals(DataType.TENSOR);
+ }
+
+ private void disallowIndexingOfMaps(AbstractSearchCluster cluster, SearchDefinition def, Field field) {
+ DataType fieldType = field.getDataType();
+ if ((fieldType instanceof MapDataType) && (((SDField) field).doesIndexing())) {
+ throw new IllegalArgumentException("Field type '" + fieldType.getName() + "' cannot be indexed for search " +
+ "clusters (field '" + field.getName() + "' in definition '" +
+ def.getName() + "' for cluster '" + cluster.getClusterName() + "').");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/StreamingValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/StreamingValidator.java
new file mode 100644
index 00000000000..9d6d10f2861
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/StreamingValidator.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import java.util.List;
+import java.util.logging.Level;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.document.NumericDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.search.SearchCluster;
+
+
+/**
+ * Validates streaming mode
+ */
+public class StreamingValidator extends Validator {
+
+ @Override
+ public void validate(VespaModel model, DeployState deployState) {
+ List<AbstractSearchCluster> searchClusters = model.getSearchClusters();
+ for (AbstractSearchCluster cluster : searchClusters) {
+ if (!cluster.isStreaming()) {
+ continue;
+ }
+ SearchCluster sc = (SearchCluster) cluster;
+ warnStreamingAttributes(sc, deployState.getDeployLogger());
+ warnStreamingGramMatching(sc, deployState.getDeployLogger());
+ }
+ }
+
+ private void warnStreamingGramMatching(SearchCluster sc, DeployLogger logger) {
+ if (sc.getSdConfig() != null) {
+ for (SDField sd : sc.getSdConfig().getSearch().allFieldsList()) {
+ if (sd.getMatching().getType().equals(Matching.Type.GRAM)) {
+ logger.log(Level.WARNING, "For streaming search cluster '" + sc.getClusterName() +
+ "', SD field '" + sd.getName() + "': n-gram matching is not supported for streaming search.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Warn if one or more attributes are defined in a streaming search cluster SD.
+ *
+ * @param sc a search cluster to be checked for attributes in streaming search
+ * @param logger a DeployLogger
+ */
+ private void warnStreamingAttributes(SearchCluster sc, DeployLogger logger) {
+ if (sc.getSdConfig() != null) {
+ for (SDField sd : sc.getSdConfig().getSearch().allFieldsList()) {
+ if (sd.doesAttributing()) {
+ warnStreamingAttribute(sc, sd, logger);
+ }
+ }
+ }
+ }
+
+ private void warnStreamingAttribute(SearchCluster sc, SDField sd, DeployLogger logger) {
+ // If the field is numeric, we can't print this, because we may have converted the field to
+ // attribute indexing ourselves (IntegerIndex2Attribute)
+ if (sd.getDataType() instanceof NumericDataType) return;
+ logger.log(Level.WARNING, "For streaming search cluster '" + sc.getClusterName() +
+ "', SD field '" + sd.getName() + "': 'attribute' has same match semantics as 'index'.");
+ }
+} \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java
new file mode 100644
index 00000000000..0c74fddcc51
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validation.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.Model;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.change.ChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.ClusterSizeReductionValidator;
+import com.yahoo.vespa.model.application.validation.change.ConfigValueChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.ContainerRestartValidator;
+import com.yahoo.vespa.model.application.validation.change.ContentClusterRemovalValidator;
+import com.yahoo.vespa.model.application.validation.change.IndexedSearchClusterChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.IndexingModeChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.StartupCommandChangeValidator;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Executor of validators. This defines the right order of validator execution.
+ * Validators that must be run after search cluster search definition deriving are
+ * defined in PostSdValidation.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 2010-01-29
+ */
+public class Validation {
+
+ /** Validate everything */
+ public static List<ConfigChangeAction> validate(VespaModel model, boolean force, DeployState deployState) {
+ return validate(model, true, force, deployState);
+ }
+
+ /**
+ * Validate with optional checking of routing, which cannot always be valid in unit tests
+ *
+ * @return a list of required changes needed to make this configuration live
+ */
+ public static List<ConfigChangeAction> validate(VespaModel model, boolean checkRouting, boolean force, DeployState deployState) {
+ if (checkRouting) {
+ new RoutingValidator().validate(model, deployState);
+ new RoutingSelectorValidator().validate(model, deployState);
+ }
+ new ComponentValidator().validate(model, deployState);
+ new SearchDataTypeValidator().validate(model, deployState);
+ new StreamingValidator().validate(model, deployState);
+ new RankSetupValidator(force).validate(model, deployState);
+ new NoPrefixForIndexes().validate(model, deployState);
+ new DeploymentFileValidator().validate(model, deployState);
+
+ Optional<Model> currentActiveModel = deployState.getPreviousModel();
+ if (currentActiveModel.isPresent() && (currentActiveModel.get() instanceof VespaModel))
+ return validateChanges((VespaModel)currentActiveModel.get(), model,
+ deployState.validationOverrides(), deployState.getDeployLogger());
+ else
+ return new ArrayList<>();
+ }
+
+ private static List<ConfigChangeAction> validateChanges(VespaModel currentModel, VespaModel nextModel,
+ ValidationOverrides overrides, DeployLogger logger) {
+ ChangeValidator[] validators = new ChangeValidator[] {
+ new IndexingModeChangeValidator(),
+ new IndexedSearchClusterChangeValidator(),
+ new ConfigValueChangeValidator(logger),
+ new StartupCommandChangeValidator(),
+ new ContentClusterRemovalValidator(),
+ new ClusterSizeReductionValidator(),
+ new ContainerRestartValidator(),
+ };
+ return Arrays.stream(validators)
+ .flatMap(v -> v.validate(currentModel, nextModel, overrides).stream())
+ .collect(toList());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationId.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationId.java
new file mode 100644
index 00000000000..cb60e6fc77f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationId.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import java.util.Optional;
+
+/**
+ * Ids of validations that can be overridden
+ *
+ * @author bratseth
+ */
+public enum ValidationId {
+
+ indexingChange("indexing-change"),
+ indexModeChange("indexing-mode-change"),
+ fieldTypeChange("field-type-change"),
+ clusterSizeReduction("cluster-size-reduction"),
+ contentClusterRemoval("content-cluster-removal"),
+ configModelVersionMismatch("config-model-version-mismatch"),
+ skipOldConfigModels("skip-old-config-models");
+
+ private final String id;
+
+ ValidationId(String id) { this.id = id; }
+
+ public String value() { return id; }
+
+ @Override
+ public String toString() { return id; }
+
+ /**
+ * Returns the validation id from this string.
+ * Use this instead of valueOf to match string on the (canonical) dash-separated form.
+ *
+ * @return the matching validation id or empty if none
+ */
+ public static Optional<ValidationId> from(String id) {
+ for (ValidationId candidate : ValidationId.values())
+ if (id.equals(candidate.toString())) return Optional.of(candidate);
+ return Optional.empty();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationOverrides.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationOverrides.java
new file mode 100644
index 00000000000..6a12d206189
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/ValidationOverrides.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.google.common.collect.ImmutableList;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * A set of allows which suppresses specific validations in limited time periods.
+ * This is useful to be able to complete a deployment in cases where the application
+ * owner believes that the changes to be deployed have acceptable consequences.
+ * Immutable.
+ *
+ * @author bratseth
+ */
+public class ValidationOverrides {
+
+ private final List<Allow> overrides;
+
+ /** Instant to use as "now". This is a field to allow unit testing. */
+ private final Instant now;
+
+ /** Creates validation overrides for the current instant */
+ public ValidationOverrides(List<Allow> overrides) {
+ this(overrides, Instant.now());
+ }
+
+ public ValidationOverrides(List<Allow> overrides, Instant now) {
+ this.overrides = ImmutableList.copyOf(overrides);
+ this.now = now;
+ for (Allow override : overrides)
+ if (now.plus(Duration.ofDays(30)).isBefore(override.until))
+ throw new IllegalArgumentException(override + " is too far in the future: Max 30 days is allowed");
+ }
+
+ /** Throws a ValidationException unless this validation is overridden at this time */
+ public void invalid(ValidationId validationId, String message) {
+ if ( ! allows(validationId))
+ throw new ValidationException(validationId, message);
+ }
+
+ public boolean allows(String validationIdString) {
+ Optional<ValidationId> validationId = ValidationId.from(validationIdString);
+ if ( ! validationId.isPresent()) return false; // unknown id -> not allowed
+ return allows(validationId.get());
+ }
+
+ /** Returns whether the given (assumed invalid) change is allowed by this at the moment */
+ public boolean allows(ValidationId validationId) {
+ for (Allow override : overrides)
+ if (override.allows(validationId, now))
+ return true;
+ return false;
+ }
+
+ public static ValidationOverrides empty() { return new ValidationOverrides(ImmutableList.of()); }
+
+ /** A validation override which allows a particular change. Immutable. */
+ public static class Allow {
+
+ private final ValidationId validationId;
+ private final Instant until;
+
+ public Allow(ValidationId validationId, Instant until) {
+ this.validationId = validationId;
+ this.until = until;
+ }
+
+ public boolean allows(ValidationId validationId, Instant now) {
+ return this.validationId.equals(validationId) && now.isBefore(until);
+ }
+
+ @Override
+ public String toString() { return "allow '" + validationId + "' until " + until; }
+
+ }
+
+ /**
+ * A deployment validation exception.
+ * Deployment validations can be {@link ValidationOverrides overridden} based on their id.
+ * The purpose of this exception is to model that id as a separate field.
+ */
+ public static class ValidationException extends IllegalArgumentException {
+
+ private final ValidationId validationId;
+
+ private ValidationException(ValidationId validationId, String message) {
+ super(message);
+ this.validationId = validationId;
+ }
+
+ /** Returns the unique id of this validation, which can be used to {@link ValidationOverrides override} it */
+ public ValidationId validationId() { return validationId; }
+
+ /** Returns "validationId: message" */
+ @Override
+ public String getMessage() { return validationId + ": " + super.getMessage(); }
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validator.java
new file mode 100644
index 00000000000..d92e15fea66
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/Validator.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.vespa.model.VespaModel;
+
+/**
+ * Abstract superclass of all application package validators.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 2010-01-29
+ */
+public abstract class Validator {
+
+ /**
+ * Validates the input vespamodel
+ *
+ * @param model a VespaModel object
+ * @param deployState The {@link DeployState} built from building the model
+ */
+ public abstract void validate(VespaModel model, DeployState deployState);
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ChangeValidator.java
new file mode 100644
index 00000000000..f60b1871e59
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ChangeValidator.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+
+import java.util.List;
+
+/**
+ * Interface for validating changes between a current active and next config model.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-11-18
+ */
+public interface ChangeValidator {
+
+ /**
+ * Validates the current active vespa model with the next model.
+ * Both current and next should be non-null.
+ *
+ * @param current the current active model
+ * @param next the next model we would like to activate
+ * @param overrides validation overrides
+ * @return a list of actions specifying what needs to be done in order to activate the new model.
+ * Return an empty list if nothing needs to be done
+ */
+ List<ConfigChangeAction> validate(VespaModel current, VespaModel next, ValidationOverrides overrides);
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java
new file mode 100644
index 00000000000..279f74db46d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidator.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationId;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Checks that no cluster sizes are reduced too much in one go.
+ *
+ * @author bratseth
+ */
+public class ClusterSizeReductionValidator implements ChangeValidator {
+
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel current, VespaModel next, ValidationOverrides overrides) {
+ for (ContainerCluster currentCluster : current.getContainerClusters().values()) {
+ ContainerCluster nextCluster = next.getContainerClusters().get(currentCluster.getName());
+ if (nextCluster == null) continue;
+ validate(currentCluster.getContainers().size(),
+ nextCluster.getContainers().size(),
+ currentCluster.getName(),
+ overrides);
+ }
+
+ for (ContentCluster currentCluster : current.getContentClusters().values()) {
+ ContentCluster nextCluster = next.getContentClusters().get(currentCluster.getName());
+ if (nextCluster == null) continue;
+ validate(currentCluster.getSearch().getSearchNodes().size(),
+ nextCluster.getSearch().getSearchNodes().size(),
+ currentCluster.getName(),
+ overrides);
+ }
+
+ return Collections.emptyList();
+ }
+
+ private void validate(int currentSize, int nextSize, String clusterName, ValidationOverrides overrides) {
+ // don't allow more than 50% reduction, but always allow to reduce size with 1
+ if ( nextSize < ((double)currentSize) * 0.5 && nextSize != currentSize - 1)
+ overrides.invalid(ValidationId.clusterSizeReduction,
+ "Size reduction in '" + clusterName + "' is too large. Current size: " + currentSize +
+ ", new size: " + nextSize + ". New size must be at least 50% of the current size");
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidator.java
new file mode 100644
index 00000000000..06d7c358678
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidator.java
@@ -0,0 +1,162 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.ChangesRequiringRestart;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.ChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.VespaRestartAction;
+import com.yahoo.vespa.model.utils.internal.ReflectionUtil;
+import org.apache.commons.lang3.ClassUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.stream.Collectors.joining;
+
+/**
+ * Compares the config instances in the current and next Vespa model to determine if some services will require restart.
+ * The configs used by a given service is deduced from the
+ * {@link com.yahoo.vespa.model.application.validation.RestartConfigs} annotation.
+ *
+ * @author bjorncs
+ */
+public class ConfigValueChangeValidator implements ChangeValidator {
+
+ private final DeployLogger logger;
+
+ public ConfigValueChangeValidator(DeployLogger logger) {
+ this.logger = logger;
+ }
+
+ /** Inspects the configuration in the new and old Vespa model to determine which services that require restart */
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel currentModel, VespaModel nextModel,
+ ValidationOverrides overrides) {
+ return findConfigChangesFromModels(currentModel, nextModel).collect(Collectors.toList());
+ }
+
+ public Stream<ConfigChangeAction> findConfigChangesFromModels(
+ AbstractConfigProducerRoot currentModel,
+ AbstractConfigProducerRoot nextModel) {
+ return nextModel.getDescendantServices().stream()
+ .map(service -> findConfigChangeActionForService(service, currentModel, nextModel))
+ .filter(Optional::isPresent)
+ .map(Optional::get);
+ }
+
+ private Optional<ConfigChangeAction> findConfigChangeActionForService(
+ Service service,
+ AbstractConfigProducerRoot currentModel,
+ AbstractConfigProducerRoot nextModel) {
+ List<ChangesRequiringRestart> changes = findConfigChangesForService(service, currentModel, nextModel)
+ .collect(Collectors.toList());
+ if (changes.isEmpty()) {
+ return Optional.empty();
+ }
+ String description = createDescriptionOfConfigChanges(changes.stream());
+ return Optional.of(new VespaRestartAction(description, service.getServiceInfo()));
+ }
+
+ private Stream<ChangesRequiringRestart> findConfigChangesForService(
+ Service service,
+ AbstractConfigProducerRoot currentModel,
+ AbstractConfigProducerRoot nextModel) {
+ Class<? extends Service> serviceClass = service.getClass();
+ if (!currentModel.getService(service.getConfigId()).isPresent()) {
+ // Service does not exist in the current model.
+ return Stream.empty();
+ }
+ return getConfigInstancesFromServiceAnnotations(serviceClass)
+ .map(configClass -> compareConfigFromCurrentAndNextModel(service, configClass, currentModel, nextModel))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .filter(ChangesRequiringRestart::needsRestart);
+ }
+
+ private static String createDescriptionOfConfigChanges(Stream<ChangesRequiringRestart> changesStream) {
+ return changesStream
+ .map(changes -> changes.toString(""))
+ .collect(joining("\n"));
+ }
+
+ /**
+ * Returns the ConfigInstances classes from the annotation on the given Service class ,
+ * including the ConfigInstances annotated on any of its super classes.
+ * NOTE: Only the super classes that are subclass of Service are inspected.
+ */
+ private static Stream<Class<? extends ConfigInstance>> getConfigInstancesFromServiceAnnotations(Class<? extends Service> serviceClass) {
+ List<Class<?>> classHierarchy = ClassUtils.getAllSuperclasses(serviceClass);
+ classHierarchy.add(serviceClass);
+ return classHierarchy.stream()
+ .filter(Service.class::isAssignableFrom)
+ .filter(clazz -> clazz.isAnnotationPresent(RestartConfigs.class))
+ .map(clazz -> {
+ RestartConfigs annotation = clazz.getDeclaredAnnotation(RestartConfigs.class);
+ if (annotation.value().length == 0) {
+ throw new IllegalStateException(String.format(
+ "%s has a %s annotation with no ConfigInstances given as argument.",
+ clazz.getSimpleName(), RestartConfigs.class.getSimpleName()));
+ }
+ return annotation;
+ })
+ .map(RestartConfigs::value)
+ .flatMap(Arrays::stream)
+ .distinct();
+ }
+
+ private Optional<ChangesRequiringRestart> compareConfigFromCurrentAndNextModel(
+ Service service, Class<? extends ConfigInstance> configClass,
+ AbstractConfigProducerRoot currentModel, AbstractConfigProducerRoot nextModel) {
+
+ if (!hasConfigFieldsFlaggedWithRestart(configClass, service.getClass())) {
+ logger.log(Level.FINE, String.format("%s is listed in the annotation for %s, " +
+ "but does not have any restart flags in its config definition.",
+ configClass.getSimpleName(), service.getClass().getSimpleName()));
+ return Optional.empty();
+ }
+
+ Optional<ConfigInstance> nextConfig = getConfigFromModel(nextModel, configClass, service.getConfigId());
+ if (!nextConfig.isPresent()) {
+ logger.log(Level.FINE, String.format(
+ "%s is listed as restart config for %s, but the config does not exist in the new model.",
+ configClass.getSimpleName(), service.getClass().getSimpleName()));
+ return Optional.empty();
+ }
+
+ Optional<ConfigInstance> currentConfig = getConfigFromModel(currentModel, configClass, service.getConfigId());
+ if (!currentConfig.isPresent()) {
+ return Optional.empty();
+ }
+ return Optional.of(ReflectionUtil.getChangesRequiringRestart(currentConfig.get(), nextConfig.get()));
+ }
+
+ private static boolean hasConfigFieldsFlaggedWithRestart(
+ Class<? extends ConfigInstance> configClass, Class<? extends Service> serviceClass) {
+ if (!ReflectionUtil.hasRestartMethods(configClass)) {
+ throw new IllegalStateException(String.format(
+ "%s is listed as restart config for %s but does not contain any restart inspection methods.",
+ configClass.getSimpleName(), serviceClass.getSimpleName()));
+ }
+ return ReflectionUtil.containsFieldsFlaggedWithRestart(configClass);
+ }
+
+ private static Optional<ConfigInstance> getConfigFromModel(
+ AbstractConfigProducerRoot configModel, Class<? extends ConfigInstance> configClass, String configKey) {
+ try {
+ return Optional.ofNullable(configModel.getConfig(configClass, configKey));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidator.java
new file mode 100644
index 00000000000..3d56fe200d8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidator.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.container.QrConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.container.Container;
+
+import java.util.List;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Returns a restart action for each container that has turned on {@link QrConfig#restartOnDeploy}.
+ *
+ * @author bjorncs
+ */
+public class ContainerRestartValidator implements ChangeValidator {
+
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel currentModel, VespaModel nextModel, ValidationOverrides ignored) {
+ return nextModel.getContainerClusters().values().stream()
+ .flatMap(cluster -> cluster.getContainers().stream())
+ .filter(container -> isExistingContainer(container, currentModel))
+ .filter(container -> shouldContainerRestartOnDeploy(container, nextModel))
+ .map(ContainerRestartValidator::createConfigChangeAction)
+ .collect(toList());
+ }
+
+ private static ConfigChangeAction createConfigChangeAction(Container container) {
+ return new VespaRestartAction(createMessage(container), container.getServiceInfo());
+ }
+
+ private static String createMessage(Container container) {
+ return String.format("Container '%s' is configured to always restart on deploy.", container.getConfigId());
+ }
+
+ private static boolean shouldContainerRestartOnDeploy(Container container, VespaModel nextModel) {
+ QrConfig config = nextModel.getConfig(QrConfig.class, container.getConfigId());
+ return config.restartOnDeploy();
+ }
+
+ private static boolean isExistingContainer(Container container, VespaModel currentModel) {
+ return currentModel.getService(container.getConfigId()).isPresent();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidator.java
new file mode 100644
index 00000000000..57e9d124c77
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidator.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationId;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Checks that this does not remove a content cluster (or changes its id)
+ * as that means losing all data of that cluster.
+ *
+ * @author bratseth
+ */
+public class ContentClusterRemovalValidator implements ChangeValidator {
+
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel current, VespaModel next, ValidationOverrides overrides) {
+ for (String currentClusterId : current.getContentClusters().keySet()) {
+ ContentCluster nextCluster = next.getContentClusters().get(currentClusterId);
+ if (nextCluster == null)
+ overrides.invalid(ValidationId.contentClusterRemoval,
+ "Content cluster '" + currentClusterId + "' is removed. " +
+ "This will cause loss of all data in this cluster");
+ }
+
+ return Collections.emptyList();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidator.java
new file mode 100644
index 00000000000..ab6bc5a1cd7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidator.java
@@ -0,0 +1,98 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.search.DocumentDatabaseChangeValidator;
+import com.yahoo.vespa.model.content.ContentSearchCluster;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.search.DocumentDatabase;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Validates the changes between all current and next indexed search clusters in a vespa model.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-11-18
+ */
+public class IndexedSearchClusterChangeValidator implements ChangeValidator {
+
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel current, VespaModel next, ValidationOverrides overrides) {
+ List<ConfigChangeAction> result = new ArrayList<>();
+ for (Map.Entry<String, ContentCluster> currentEntry : current.getContentClusters().entrySet()) {
+ ContentCluster nextCluster = next.getContentClusters().get(currentEntry.getKey());
+ if (nextCluster != null && nextCluster.getSearch().hasIndexedCluster()) {
+ result.addAll(validateContentCluster(currentEntry.getValue(), nextCluster, overrides));
+ }
+ }
+ return result;
+ }
+
+ private static List<ConfigChangeAction> validateContentCluster(ContentCluster currentCluster,
+ ContentCluster nextCluster,
+ ValidationOverrides overrides) {
+ List<ConfigChangeAction> result = new ArrayList<>();
+ result.addAll(validateDocumentDatabases(currentCluster, nextCluster, overrides));
+ return result;
+ }
+
+ private static List<ConfigChangeAction> validateDocumentDatabases(ContentCluster currentCluster,
+ ContentCluster nextCluster,
+ ValidationOverrides overrides) {
+ List<ConfigChangeAction> result = new ArrayList<>();
+ for (DocumentDatabase currentDb : getDocumentDbs(currentCluster.getSearch())) {
+ String docTypeName = currentDb.getName();
+ Optional<DocumentDatabase> nextDb = nextCluster.getSearch().getIndexed().getDocumentDbs().stream().
+ filter(db -> db.getName().equals(docTypeName)).findFirst();
+ if (nextDb.isPresent()) {
+ result.addAll(validateDocumentDatabase(currentCluster, nextCluster, docTypeName,
+ currentDb, nextDb.get(), overrides));
+ }
+ }
+ return result;
+ }
+
+ private static List<ConfigChangeAction> validateDocumentDatabase(ContentCluster currentCluster,
+ ContentCluster nextCluster,
+ String docTypeName,
+ DocumentDatabase currentDb,
+ DocumentDatabase nextDb,
+ ValidationOverrides overrides) {
+ NewDocumentType currentDocType = currentCluster.getDocumentDefinitions().get(docTypeName);
+ NewDocumentType nextDocType = nextCluster.getDocumentDefinitions().get(docTypeName);
+ List<VespaConfigChangeAction> result =
+ new DocumentDatabaseChangeValidator(currentDb, currentDocType, nextDb, nextDocType).validate(overrides);
+
+ return modifyActions(result, getSearchNodeServices(nextCluster.getSearch().getIndexed()), docTypeName);
+ }
+
+ private static List<DocumentDatabase> getDocumentDbs(ContentSearchCluster cluster) {
+ if (cluster.getIndexed() != null) {
+ return cluster.getIndexed().getDocumentDbs();
+ }
+ return new ArrayList<>();
+ }
+
+ private static List<ServiceInfo> getSearchNodeServices(IndexedSearchCluster cluster) {
+ return cluster.getSearchNodes().stream().
+ map(node -> node.getServiceInfo()).
+ collect(Collectors.toList());
+ }
+
+ private static List<ConfigChangeAction> modifyActions(List<VespaConfigChangeAction> result,
+ List<ServiceInfo> services,
+ String docTypeName) {
+ return result.stream().
+ map(action -> action.modifyAction("Document type '" + docTypeName + "': " + action.getMessage(),
+ services, docTypeName)).
+ collect(Collectors.toList());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidator.java
new file mode 100644
index 00000000000..18683ce2411
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidator.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.ChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.VespaRefeedAction;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Returns any change to the indexing mode of a cluster.
+ *
+ * @author musum
+ */
+public class IndexingModeChangeValidator implements ChangeValidator {
+
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel currentModel, VespaModel nextModel, ValidationOverrides overrides) {
+ List<ConfigChangeAction> actions = new ArrayList<>();
+ for (Map.Entry<String, ContentCluster> currentEntry : currentModel.getContentClusters().entrySet()) {
+ ContentCluster nextCluster = nextModel.getContentClusters().get(currentEntry.getKey());
+ if (nextCluster == null) continue;
+
+ Optional<ConfigChangeAction> change = validateContentCluster(currentEntry.getValue(), nextCluster, overrides);
+ if (change.isPresent())
+ actions.add(change.get());
+ }
+ return actions;
+ }
+
+ private Optional<ConfigChangeAction> validateContentCluster(ContentCluster currentCluster, ContentCluster nextCluster,
+ ValidationOverrides overrides) {
+ final boolean currentClusterIsIndexed = currentCluster.getSearch().hasIndexedCluster();
+ final boolean nextClusterIsIndexed = nextCluster.getSearch().hasIndexedCluster();
+
+ if (currentClusterIsIndexed == nextClusterIsIndexed) return Optional.empty();
+
+ return Optional.of(VespaRefeedAction.of("indexing-mode-change",
+ overrides,
+ "Cluster '" + currentCluster.getName() + "' changed indexing mode from '" +
+ indexingMode(currentClusterIsIndexed) + "' to '" + indexingMode(nextClusterIsIndexed) + "'"));
+ }
+
+ private String indexingMode(boolean isIndexed) {
+ return isIndexed ? "indexed" : "streaming";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidator.java
new file mode 100644
index 00000000000..fb90de60a2f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidator.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.ChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.VespaRestartAction;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Compares the startup command for the services in the next model with the ones in the old model.
+ * If the startup command has changes, a change entry is created and reported back.
+ *
+ * @author bjorncs
+ */
+public class StartupCommandChangeValidator implements ChangeValidator {
+
+ @Override
+ public List<ConfigChangeAction> validate(VespaModel currentModel, VespaModel nextModel,
+ ValidationOverrides overrides) {
+ return findServicesWithChangedStartupCommmand(currentModel, nextModel).collect(Collectors.toList());
+ }
+
+ public Stream<ConfigChangeAction> findServicesWithChangedStartupCommmand(
+ AbstractConfigProducerRoot currentModel, AbstractConfigProducerRoot nextModel) {
+ return nextModel.getDescendantServices().stream()
+ .map(nextService -> currentModel.getService(nextService.getConfigId())
+ .flatMap(currentService -> compareStartupCommand(currentService, nextService)))
+ .filter(Optional::isPresent)
+ .map(Optional::get);
+ }
+
+ private Optional<ConfigChangeAction> compareStartupCommand(Service currentService, Service nextService) {
+ String currentCommand = currentService.getStartupCommand();
+ String nextCommand = nextService.getStartupCommand();
+
+ // Objects.equals is null-aware
+ if (Objects.equals(currentCommand, nextCommand)) {
+ return Optional.empty();
+ }
+ String message = String.format("Startup command for '%s' has changed.\nNew command: %s.\nOld command: %s.",
+ currentService.getServiceName(), nextCommand, currentCommand);
+ return Optional.of(new VespaRestartAction(message, currentService.getServiceInfo()));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaConfigChangeAction.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaConfigChangeAction.java
new file mode 100644
index 00000000000..a8a334448ff
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaConfigChangeAction.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Class containing the action to be performed on the given services to handle a config change
+ * between the current active vespa model and the next vespa model to prepare.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-11-18
+ */
+public abstract class VespaConfigChangeAction implements ConfigChangeAction {
+
+ private final String message;
+ private final List<ServiceInfo> services;
+
+ protected VespaConfigChangeAction(String message, List<ServiceInfo> services) {
+ this.message = message;
+ this.services = services;
+ }
+
+ public abstract VespaConfigChangeAction modifyAction(String newMessage, List<ServiceInfo> newServices, String documentType);
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public List<ServiceInfo> getServices() {
+ return services;
+ }
+
+ @Override
+ public String toString() {
+ return "type='" + getType() + "', message='" + message + "', services=[" +
+ services.stream().
+ map(service -> service.getServiceName() + " '" + service.getConfigId() + "'").
+ collect(Collectors.joining(", ")) + "]";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof VespaConfigChangeAction)) {
+ return false;
+ }
+ VespaConfigChangeAction rhs = (VespaConfigChangeAction)o;
+ if (!getType().equals(rhs.getType())) return false;
+ if (!message.equals(rhs.message)) return false;
+ if (!services.equals(rhs.services)) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = getType().hashCode();
+ result = 31 * result + message.hashCode();
+ result = 31 * result + services.hashCode();
+ return result;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRefeedAction.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRefeedAction.java
new file mode 100644
index 00000000000..6b8d18fadea
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRefeedAction.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeRefeedAction;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents an action to re-feed a document type in order to handle a config change.
+ *
+ * @author geirst
+ * @author bratseth
+ * @since 5.43
+ */
+public class VespaRefeedAction extends VespaConfigChangeAction implements ConfigChangeRefeedAction {
+
+ /**
+ * The name of this action, which must be a valid ValidationId. This is a string here because
+ * the validation ids belong to the Vespa model while these names are exposed to the config server,
+ * which is model version independent.
+ */
+ private final String name;
+
+ private final String documentType;
+ private final boolean allowed;
+
+ private VespaRefeedAction(String name, String message, List<ServiceInfo> services, String documentType, boolean allowed) {
+ super(message, services);
+ this.name = name;
+ this.documentType = documentType;
+ this.allowed = allowed;
+ }
+
+ /** Creates a refeed action with some missing information */
+ // TODO: We should require document type or model its absence properly
+ public static VespaRefeedAction of(String name, ValidationOverrides overrides, String message) {
+ return new VespaRefeedAction(name, message, Collections.emptyList(), "", overrides.allows(name));
+ }
+
+ /** Creates a refeed action */
+ public static VespaRefeedAction of(String name, ValidationOverrides overrides, String message,
+ List<ServiceInfo> services, String documentType) {
+ return new VespaRefeedAction(name, message, services, documentType, overrides.allows(name));
+ }
+
+ @Override
+ public VespaConfigChangeAction modifyAction(String newMessage, List<ServiceInfo> newServices, String documentType) {
+ return new VespaRefeedAction(name, newMessage, newServices, documentType, allowed);
+ }
+
+ @Override
+ public String name() { return name; }
+
+ @Override
+ public String getDocumentType() { return documentType; }
+
+ @Override
+ public boolean allowed() { return allowed; }
+
+ @Override
+ public String toString() {
+ return super.toString() + ", documentType='" + documentType + "'";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( ! super.equals(o)) return false;
+ if ( ! (o instanceof VespaRefeedAction)) return false;
+ VespaRefeedAction other = (VespaRefeedAction)o;
+ if ( ! this.documentType.equals(other.documentType)) return false;
+ if ( ! this.name.equals(other.name)) return false;
+ if ( ! this.allowed == other.allowed) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * super.hashCode() + 11 * name.hashCode() + documentType.hashCode();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRestartAction.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRestartAction.java
new file mode 100644
index 00000000000..1974847a83c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/VespaRestartAction.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeRestartAction;
+import com.yahoo.config.model.api.ServiceInfo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents an action to restart services in order to handle a config change.
+ *
+ * @author geirst
+ * @since 5.43
+ */
+public class VespaRestartAction extends VespaConfigChangeAction implements ConfigChangeRestartAction {
+
+ public VespaRestartAction(String message) {
+ super(message, new ArrayList<>());
+ }
+
+ public VespaRestartAction(String message, ServiceInfo service) {
+ super(message, Collections.singletonList(service));
+ }
+
+ public VespaRestartAction(String message, List<ServiceInfo> services) {
+ super(message, services);
+ }
+
+ @Override
+ public VespaConfigChangeAction modifyAction(String newMessage, List<ServiceInfo> newServices, String documentType) {
+ return new VespaRestartAction(newMessage, newServices);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java
new file mode 100644
index 00000000000..9e057c6cd28
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidator.java
@@ -0,0 +1,116 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.searchdefinition.derived.AttributeFields;
+import com.yahoo.searchdefinition.derived.IndexSchema;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.application.validation.change.VespaRestartAction;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Validates the changes between the current and next set of attribute fields in a document database.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-12-04
+ */
+public class AttributeChangeValidator {
+
+ private final AttributeFields currentFields;
+ private final IndexSchema currentIndexSchema;
+ private final NewDocumentType currentDocType;
+ private final AttributeFields nextFields;
+ private final IndexSchema nextIndexSchema;
+ private final NewDocumentType nextDocType;
+
+ public AttributeChangeValidator(AttributeFields currentFields,
+ IndexSchema currentIndexSchema,
+ NewDocumentType currentDocType,
+ AttributeFields nextFields,
+ IndexSchema nextIndexSchema,
+ NewDocumentType nextDocType) {
+ this.currentFields = currentFields;
+ this.currentIndexSchema = currentIndexSchema;
+ this.currentDocType = currentDocType;
+ this.nextFields = nextFields;
+ this.nextIndexSchema = nextIndexSchema;
+ this.nextDocType = nextDocType;
+ }
+
+ public List<VespaConfigChangeAction> validate() {
+ List<VespaConfigChangeAction> result = new ArrayList<>();
+ result.addAll(validateAddAttributeAspect());
+ result.addAll(validateRemoveAttributeAspect());
+ result.addAll(validateAttributeSettings());
+ return result;
+ }
+
+ private List<VespaConfigChangeAction> validateAddAttributeAspect() {
+ return nextFields.attributes().stream().
+ map(attr -> attr.getName()).
+ filter(attrName -> !currentFields.containsAttribute(attrName) &&
+ currentDocType.containsField(attrName)).
+ map(attrName -> new VespaRestartAction(new ChangeMessageBuilder(attrName).
+ addChange("add attribute aspect").build())).
+ collect(Collectors.toList());
+ }
+
+ private List<VespaConfigChangeAction> validateRemoveAttributeAspect() {
+ return currentFields.attributes().stream().
+ map(attr -> attr.getName()).
+ filter(attrName -> !nextFields.containsAttribute(attrName) &&
+ nextDocType.containsField(attrName) &&
+ !isIndexField(attrName)).
+ map(attrName -> new VespaRestartAction(new ChangeMessageBuilder(attrName).
+ addChange("remove attribute aspect").build())).
+ collect(Collectors.toList());
+ }
+
+ private boolean isIndexField(String fieldName) {
+ return currentIndexSchema.containsField(fieldName) && nextIndexSchema.containsField(fieldName);
+ }
+
+ private List<VespaConfigChangeAction> validateAttributeSettings() {
+ List<VespaConfigChangeAction> result = new ArrayList<>();
+ for (Attribute nextAttr : nextFields.attributes()) {
+ Attribute currAttr = currentFields.getAttribute(nextAttr.getName());
+ if (currAttr != null) {
+ validateAttributeSetting(currAttr, nextAttr, Attribute::isFastSearch, "fast-search", result);
+ validateAttributeSetting(currAttr, nextAttr, Attribute::isFastAccess, "fast-access", result);
+ validateAttributeSetting(currAttr, nextAttr, Attribute::isHuge, "huge", result);
+ validateAttributeSetting(currAttr, nextAttr, Attribute::densePostingListThreshold, "dense-posting-list-threshold", result);
+ }
+ }
+ return result;
+ }
+
+ private static void validateAttributeSetting(Attribute currentAttr, Attribute nextAttr,
+ Predicate<Attribute> predicate, String setting,
+ List<VespaConfigChangeAction> result) {
+ final boolean nextValue = predicate.test(nextAttr);
+ if (predicate.test(currentAttr) != nextValue) {
+ String change = nextValue ? "add" : "remove";
+ result.add(new VespaRestartAction(new ChangeMessageBuilder(nextAttr.getName()).
+ addChange(change + " attribute '" + setting + "'").build()));
+ }
+ }
+
+ private static <T> void validateAttributeSetting(Attribute currentAttr, Attribute nextAttr,
+ Function<Attribute, T> settingValueProvider, String setting,
+ List<VespaConfigChangeAction> result) {
+ T currentValue = settingValueProvider.apply(currentAttr);
+ T nextValue = settingValueProvider.apply(nextAttr);
+ if (!Objects.equals(currentValue, nextValue)) {
+ String message = String.format("change property '%s' from '%s' to '%s'", setting, currentValue, nextValue);
+ result.add(new VespaRestartAction(new ChangeMessageBuilder(nextAttr.getName()).addChange(message).build()));
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/ChangeMessageBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/ChangeMessageBuilder.java
new file mode 100644
index 00000000000..28b04d54307
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/ChangeMessageBuilder.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class used to build a message describing the changes in a given field.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-12-09
+ */
+public class ChangeMessageBuilder {
+
+ private final String fieldName;
+ private final List<String> changes = new ArrayList<>();
+
+ public ChangeMessageBuilder(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ public String build() {
+ StringBuilder retval = new StringBuilder();
+ retval.append("Field '" + fieldName + "' changed: ");
+ retval.append(String.join(", ", changes));
+ return retval.toString();
+ }
+
+ public ChangeMessageBuilder addChange(String component, String from, String to) {
+ changes.add(component + ": '" + from + "' -> '" + to + "'");
+ return this;
+ }
+
+ public ChangeMessageBuilder addChange(String message) {
+ changes.add(message);
+ return this;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidator.java
new file mode 100644
index 00000000000..862b6caf0ca
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidator.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.search.DocumentDatabase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Validates the changes between a current and next document database that is part of an indexed search cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-11-18
+ */
+public class DocumentDatabaseChangeValidator {
+
+ private DocumentDatabase currentDatabase;
+ private NewDocumentType currentDocType;
+ private DocumentDatabase nextDatabase;
+ private NewDocumentType nextDocType;
+
+ public DocumentDatabaseChangeValidator(DocumentDatabase currentDatabase,
+ NewDocumentType currentDocType,
+ DocumentDatabase nextDatabase,
+ NewDocumentType nextDocType) {
+ this.currentDatabase = currentDatabase;
+ this.currentDocType = currentDocType;
+ this.nextDatabase = nextDatabase;
+ this.nextDocType = nextDocType;
+ }
+
+ public List<VespaConfigChangeAction> validate(ValidationOverrides overrides) {
+ List<VespaConfigChangeAction> result = new ArrayList<>();
+ result.addAll(validateAttributeChanges());
+ result.addAll(validateIndexingScriptChanges(overrides));
+ result.addAll(validateDocumentTypeChanges(overrides));
+ return result;
+ }
+
+ private List<VespaConfigChangeAction> validateAttributeChanges() {
+ return new AttributeChangeValidator(
+ currentDatabase.getDerivedConfiguration().getAttributeFields(),
+ currentDatabase.getDerivedConfiguration().getIndexSchema(), currentDocType,
+ nextDatabase.getDerivedConfiguration().getAttributeFields(),
+ nextDatabase.getDerivedConfiguration().getIndexSchema(), nextDocType).validate();
+ }
+
+ private List<VespaConfigChangeAction> validateIndexingScriptChanges(ValidationOverrides overrides) {
+ return new IndexingScriptChangeValidator(currentDatabase.getDerivedConfiguration().getSearch(),
+ nextDatabase.getDerivedConfiguration().getSearch()).validate(overrides);
+ }
+
+ private List<VespaConfigChangeAction> validateDocumentTypeChanges(ValidationOverrides overrides) {
+ return new DocumentTypeChangeValidator(currentDocType, nextDocType).validate(overrides);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidator.java
new file mode 100644
index 00000000000..4b0e350718c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidator.java
@@ -0,0 +1,160 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.document.StructDataType;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.document.Field;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.application.validation.change.VespaRefeedAction;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Validates the changes between a current and next document type used in a document database.
+ *
+ * @author <a href="mailto:Tor.Egge@yahoo-inc.com">Tor Egge</a>
+ * @since 2014-11-25
+ */
+public class DocumentTypeChangeValidator {
+
+ private NewDocumentType currentDocType;
+ private NewDocumentType nextDocType;
+
+ private static abstract class FieldChange {
+
+ protected final Field currentField;
+ protected final Field nextField;
+
+ public FieldChange(Field currentField, Field nextField) {
+ this.currentField = currentField;
+ this.nextField = nextField;
+ }
+
+ public String fieldName() {
+ return currentField.getName();
+ }
+
+ public boolean valid() {
+ return nextField != null;
+ }
+
+ public abstract boolean changedType();
+ public abstract String currentTypeName();
+ public abstract String nextTypeName();
+ }
+
+ private static class SimpleFieldChange extends FieldChange {
+
+ public SimpleFieldChange(Field currentField, Field nextField) {
+ super(currentField, nextField);
+ }
+
+ public boolean changedType() {
+ return !currentField.getDataType().equals(nextField.getDataType());
+ }
+
+ public String currentTypeName() {
+ return currentField.getDataType().getName();
+ }
+
+ public String nextTypeName() {
+ return nextField.getDataType().getName();
+ }
+ }
+
+ private static class StructFieldChange extends FieldChange {
+
+ private final StructDataType currentType;
+ private final StructDataType nextType;
+
+ public StructFieldChange(Field currentField, Field nextField) {
+ super(currentField, nextField);
+ this.currentType = (StructDataType)currentField.getDataType();
+ this.nextType = (StructDataType)nextField.getDataType();
+ }
+
+ public boolean changedType() {
+ return changedType(currentType, nextType);
+ }
+
+ public String currentTypeName() {
+ return toString(currentType);
+ }
+
+ public String nextTypeName() {
+ return toString(nextType);
+ }
+
+ private static boolean changedType(StructDataType currentType, StructDataType nextType) {
+ for (Field currentField : currentType.getFields()) {
+ Field nextField = nextType.getField(currentField.getName());
+ if (nextField != null) {
+ if (areStructFields(currentField, nextField)) {
+ if (changedType((StructDataType) currentField.getDataType(),
+ (StructDataType) nextField.getDataType())) {
+ return true;
+ }
+ } else {
+ if (!currentField.getDataType().equals(nextField.getDataType())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static String toString(StructDataType dataType) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(dataType.getName()).append(":{");
+ boolean first = true;
+ for (Field field : dataType.getFields()) {
+ if (!first) {
+ builder.append(",");
+ }
+ if (field.getDataType() instanceof StructDataType) {
+ builder.append(toString((StructDataType) field.getDataType()));
+ } else {
+ builder.append(field.getName() + ":" + field.getDataType().getName());
+ }
+ first = false;
+ }
+ builder.append("}");
+ return builder.toString();
+ }
+ }
+
+ public DocumentTypeChangeValidator(NewDocumentType currentDocType,
+ NewDocumentType nextDocType) {
+ this.currentDocType = currentDocType;
+ this.nextDocType = nextDocType;
+ }
+
+ public List<VespaConfigChangeAction> validate(ValidationOverrides overrides) {
+ return currentDocType.getAllFields().stream().
+ map(field -> createFieldChange(field, nextDocType)).
+ filter(fieldChange -> fieldChange.valid() && fieldChange.changedType()).
+ map(fieldChange -> VespaRefeedAction.of("field-type-change",
+ overrides,
+ new ChangeMessageBuilder(fieldChange.fieldName()).
+ addChange("data type", fieldChange.currentTypeName(),
+ fieldChange.nextTypeName()).build())).
+ collect(Collectors.toList());
+ }
+
+ private static FieldChange createFieldChange(Field currentField, NewDocumentType nextDocType) {
+ Field nextField = nextDocType.getField(currentField.getName());
+ if (nextField != null && areStructFields(currentField, nextField)) {
+ return new StructFieldChange(currentField, nextField);
+ }
+ return new SimpleFieldChange(currentField, nextField);
+ }
+
+ private static boolean areStructFields(Field currentField, Field nextField) {
+ return (currentField.getDataType() instanceof StructDataType) &&
+ (nextField.getDataType() instanceof StructDataType);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeMessageBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeMessageBuilder.java
new file mode 100644
index 00000000000..5d8121b6ef8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeMessageBuilder.java
@@ -0,0 +1,97 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.NormalizeLevel;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+
+/**
+ * Class used to build a message describing the usual field changes causing changes in the indexing script.
+ * This message should be more descriptive for the end-user than just seeing the changed indexing script.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-12-09
+ */
+public class IndexingScriptChangeMessageBuilder {
+
+ private final Search currentSearch;
+ private final SDField currentField;
+ private final Search nextSearch;
+ private final SDField nextField;
+
+ public IndexingScriptChangeMessageBuilder(Search currentSearch, SDField currentField,
+ Search nextSearch, SDField nextField) {
+ this.currentSearch = currentSearch;
+ this.currentField = currentField;
+ this.nextSearch = nextSearch;
+ this.nextField = nextField;
+ }
+
+ public void populate(ChangeMessageBuilder builder) {
+ checkIndexing(builder);
+ checkMatching(builder);
+ checkStemming(builder);
+ checkNormalizing(builder);
+ checkSummaryTransform(builder);
+ }
+
+ private void checkIndexing(ChangeMessageBuilder builder) {
+ if (currentField.doesIndexing() != nextField.doesIndexing()) {
+ String change = nextField.doesIndexing() ? "add" : "remove";
+ builder.addChange(change + " index aspect");
+ }
+ }
+
+ private void checkMatching(ChangeMessageBuilder builder) {
+ Matching currentMatching = currentField.getMatching();
+ Matching nextMatching = nextField.getMatching();
+ if (!currentMatching.equals(nextMatching)) {
+ builder.addChange("matching", toString(currentMatching), toString(nextMatching));
+ }
+ }
+
+ private void checkStemming(ChangeMessageBuilder builder) {
+ Stemming currentStemming = currentField.getStemming(currentSearch);
+ Stemming nextStemming = nextField.getStemming(nextSearch);
+ if (!currentStemming.equals(nextStemming)) {
+ builder.addChange("stemming", currentStemming.getName(), nextStemming.getName());
+ }
+ }
+
+ private void checkNormalizing(ChangeMessageBuilder builder) {
+ NormalizeLevel.Level currentLevel = currentField.getNormalizing().getLevel();
+ NormalizeLevel.Level nextLevel = nextField.getNormalizing().getLevel();
+ if (!currentLevel.equals(nextLevel)) {
+ builder.addChange("normalizing", currentLevel.toString(), nextLevel.toString());
+ }
+ }
+
+ private void checkSummaryTransform(ChangeMessageBuilder builder) {
+ for (SummaryField nextSummaryField : nextField.getSummaryFields()) {
+ String fieldName = nextSummaryField.getName();
+ SummaryField currentSummaryField = currentField.getSummaryField(fieldName);
+ if (currentSummaryField != null) {
+ SummaryTransform currentTransform = currentSummaryField.getTransform();
+ SummaryTransform nextTransform = nextSummaryField.getTransform();
+ if (!currentSummaryField.getTransform().equals(nextSummaryField.getTransform())) {
+ builder.addChange("summary field '" + fieldName + "' transform",
+ currentTransform.getName(), nextTransform.getName());
+ }
+ }
+ }
+ }
+
+ private static String toString(Matching matching) {
+ Matching.Type type = matching.getType();
+ String retval = type.getName();
+ if (type.equals(Matching.Type.GRAM)) {
+ retval += " (size " + matching.getGramSize() + ")";
+ }
+ return retval;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidator.java
new file mode 100644
index 00000000000..f1043f14fdc
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidator.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.indexinglanguage.ExpressionConverter;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.expressions.OutputExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.application.validation.change.VespaRefeedAction;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Validates the indexing script changes in all fields in the current and next search model.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ * @since 2014-12-08
+ */
+public class IndexingScriptChangeValidator {
+
+ private final Search currentSearch;
+ private final Search nextSearch;
+
+ public IndexingScriptChangeValidator(Search currentSearch, Search nextSearch) {
+ this.currentSearch = currentSearch;
+ this.nextSearch = nextSearch;
+ }
+
+ public List<VespaConfigChangeAction> validate(ValidationOverrides overrides) {
+ List<VespaConfigChangeAction> result = new ArrayList<>();
+ for (SDField nextField : nextSearch.allFieldsList()) {
+ String fieldName = nextField.getName();
+ SDField currentField = currentSearch.getField(fieldName);
+ if (currentField != null) {
+ validateScripts(currentField, nextField, overrides).ifPresent(r -> result.add(r));
+ }
+ }
+ return result;
+ }
+
+ private Optional<VespaConfigChangeAction> validateScripts(SDField currentField, SDField nextField,
+ ValidationOverrides overrides) {
+ ScriptExpression currentScript = currentField.getIndexingScript();
+ ScriptExpression nextScript = nextField.getIndexingScript();
+ if (!equalScripts(currentScript, nextScript)) {
+ ChangeMessageBuilder messageBuilder = new ChangeMessageBuilder(nextField.getName());
+ new IndexingScriptChangeMessageBuilder(currentSearch, currentField, nextSearch, nextField).populate(messageBuilder);
+ messageBuilder.addChange("indexing script", currentScript.toString(), nextScript.toString());
+ return Optional.of(VespaRefeedAction.of("indexing-change", overrides, messageBuilder.build()));
+ }
+ return Optional.empty();
+ }
+
+ static boolean equalScripts(ScriptExpression currentScript,
+ ScriptExpression nextScript) {
+ // Output expressions are specifying in which context a field value is used (attribute, index, summary),
+ // and do not affect how the field value is generated in the indexing doc proc.
+ // The output expressions are therefore removed before doing the comparison.
+ // Validating the addition / removal of attribute and index aspects are handled in other validators.
+ return removeOutputExpressions(currentScript).equals(removeOutputExpressions(nextScript));
+ }
+
+ private static ScriptExpression removeOutputExpressions(ScriptExpression script) {
+ ScriptExpression retval = (ScriptExpression) new OutputExpressionRemover().convert(script);
+ return retval;
+ }
+
+ private static class OutputExpressionRemover extends ExpressionConverter {
+
+ @Override
+ protected boolean shouldConvert(Expression exp) {
+ return exp instanceof OutputExpression;
+ }
+
+ @Override
+ protected Expression doConvert(Expression exp) {
+ if (exp instanceof OutputExpression) {
+ return null;
+ }
+ return exp;
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/package-info.java
new file mode 100644
index 00000000000..1e2df635ab6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/application/validation/xml/ValidationOverridesXMLReader.java b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/xml/ValidationOverridesXMLReader.java
new file mode 100644
index 00000000000..0bbc4e32010
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/application/validation/xml/ValidationOverridesXMLReader.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.xml;
+
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.application.validation.ValidationId;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import org.w3c.dom.Element;
+
+import java.io.Reader;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Reader of the validation-allows.xml file in application packages.
+ *
+ * @author bratseth
+ */
+public class ValidationOverridesXMLReader {
+
+ /**
+ * Returns a ValidationOverrides instance with the content of the given Reader.
+ * An empty ValidationOverrides is returned if the argument is empty.
+ *
+ * @param reader the reader which optionally contains a validation-overrides XML structure
+ * @param now the instant to use as "now", settable for unit testing
+ * @return a ValidationOverrides from the argument
+ * @throws IllegalArgumentException if the validation-allows.xml file exists but is invalid
+ */
+ public ValidationOverrides read(Optional<Reader> reader, Instant now) {
+ if ( ! reader.isPresent()) return ValidationOverrides.empty();
+
+ try {
+ // Assume valid structure is ensured by schema validation
+ Element root = XML.getDocument(reader.get()).getDocumentElement();
+ List<ValidationOverrides.Allow> overrides = new ArrayList<>();
+ for (Element allow : XML.getChildren(root, "allow")) {
+ Instant until = LocalDate.parse(allow.getAttribute("until"), DateTimeFormatter.ISO_DATE)
+ .atStartOfDay().atZone(ZoneOffset.UTC).toInstant()
+ .plus(Duration.ofDays(1)); // Make the override valid *on* the "until" date
+ Optional<ValidationId> validationId = ValidationId.from(XML.getValue(allow));
+ if (validationId.isPresent()) // skip unknonw ids as they may be valid for other model versions
+ overrides.add(new ValidationOverrides.Allow(validationId.get(), until));
+ }
+ return new ValidationOverrides(overrides, now);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("validation-overrides is invalid", e);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/UserConfigBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/UserConfigBuilder.java
new file mode 100644
index 00000000000..612df56cfc9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/UserConfigBuilder.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder;
+
+import com.yahoo.config.model.deploy.ConfigDefinitionStore;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.producer.UserConfigRepo;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.model.builder.xml.dom.DomConfigPayloadBuilder;
+import org.w3c.dom.Element;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class UserConfigBuilder {
+
+ public static final Logger log = Logger.getLogger(UserConfigBuilder.class.getPackage().toString());
+
+ public static UserConfigRepo build(Element producerSpec, ConfigDefinitionStore configDefinitionStore, DeployLogger deployLogger) {
+ final Map<ConfigDefinitionKey, ConfigPayloadBuilder> builderMap = new LinkedHashMap<>();
+ if (producerSpec == null) {
+ log.log(LogLevel.SPAM, "In getUserConfigs. producerSpec is null");
+ }
+ log.log(LogLevel.DEBUG, "getUserConfigs for " + producerSpec);
+ for (Element configE : XML.getChildren(producerSpec, "config")) {
+ buildElement(configE, builderMap, configDefinitionStore, deployLogger);
+ }
+ return new UserConfigRepo(builderMap);
+ }
+
+
+ private static void buildElement(Element element, Map<ConfigDefinitionKey, ConfigPayloadBuilder> builderMap, ConfigDefinitionStore configDefinitionStore, DeployLogger logger) {
+ ConfigDefinitionKey key = DomConfigPayloadBuilder.parseConfigName(element);
+ log.log(LogLevel.SPAM, "Looking at " + key);
+
+ ConfigDefinition def = getConfigDef(key, configDefinitionStore);
+ // TODO: Fail here unless deploying with :force true
+ if (def == null) {
+ logger.log(LogLevel.WARNING, "Unable to find config definition for config '" + key.getNamespace() + "." + key.getName() +
+ "'. Please ensure that the name is spelled correctly, and that the def file is included in a bundle.");
+ }
+ List<String> issuedWarnings = new ArrayList<>();
+ for (String warning : issuedWarnings) {
+ logger.log(LogLevel.WARNING, warning);
+ }
+ ConfigPayloadBuilder payloadBuilder = new DomConfigPayloadBuilder(def).build(element, issuedWarnings);
+ log.log(LogLevel.SPAM, "configvalue=" + ConfigPayload.fromBuilder(payloadBuilder).toString());
+ log.log(LogLevel.DEBUG, "Looking up key: " + key.toString());
+ ConfigPayloadBuilder old = builderMap.get(key);
+ if (old != null) {
+ logger.log(LogLevel.WARNING, "Multiple overrides for " + key + " found. Applying in the order they are discovered");
+ log.log(LogLevel.DEBUG, "old configvalue=" + old);
+ old.override(payloadBuilder);
+ } else {
+ builderMap.put(key, payloadBuilder);
+ }
+ }
+
+ /**
+ * Returns the config definition matching the given name, or null if not found.
+ */
+ private static ConfigDefinition getConfigDef(ConfigDefinitionKey configDefinitionKey, ConfigDefinitionStore configDefinitionStore) {
+ try {
+ return configDefinitionStore.getConfigDefinition(configDefinitionKey);
+ } catch (IllegalArgumentException e) {
+ log.log(LogLevel.DEBUG, "Unable to retrieve config definition: " + e.getMessage());
+ return null;
+ }
+ }
+
+}
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/VespaModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/VespaModelBuilder.java
new file mode 100644
index 00000000000..75e9caefbd5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/VespaModelBuilder.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder;
+
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+
+import java.util.List;
+
+/**
+ * Base class for classes capable of building vespa model.
+ *
+ * @author vegardh
+ */
+public abstract class VespaModelBuilder {
+
+
+ public abstract ApplicationConfigProducerRoot getRoot(String name, DeployState deployState, AbstractConfigProducer parent);
+ public abstract List<ServiceCluster> getClusters(ApplicationPackage pkg, AbstractConfigProducer parent);
+
+ /**
+ * Processing that requires access across plugins
+ * @param producerRoot The root producer.
+ * @param configModelRepo a {@link com.yahoo.config.model.ConfigModelRepo instance}
+ */
+ public abstract void postProc(AbstractConfigProducer producerRoot, ConfigModelRepo configModelRepo);
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryScaledAmountParser.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryScaledAmountParser.java
new file mode 100644
index 00000000000..9fbdee30661
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryScaledAmountParser.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.binaryprefix.BinaryPrefix;
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author tonytv
+ */
+public class BinaryScaledAmountParser {
+ //The pattern must match the one given in the schema
+ private static Pattern pattern = Pattern.compile("(\\d+(\\.\\d*)?)\\s*([kmgKMG])?");
+
+ public static BinaryScaledAmount parse(String valueString) {
+ Matcher matcher = pattern.matcher(valueString);
+
+ if (!matcher.matches()) {
+ throw new RuntimeException("Pattern and schema is out of sync.");
+ }
+
+ double amount = Double.valueOf(matcher.group(1));
+ String binaryPrefixString = matcher.group(3);
+
+ return new BinaryScaledAmount(amount, asBinaryPrefix(binaryPrefixString));
+ }
+
+ private static BinaryPrefix asBinaryPrefix(String binaryPrefixString) {
+ if (binaryPrefixString == null) {
+ return BinaryPrefix.unit;
+ } else {
+ return BinaryPrefix.fromSymbol(binaryPrefixString.toUpperCase().charAt(0));
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryUnit.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryUnit.java
new file mode 100644
index 00000000000..38d919f0f23
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/BinaryUnit.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * @author tonytv
+ */
+public class BinaryUnit {
+ //The pattern must match the one given in the schema
+ private static Pattern pattern = Pattern.compile("(\\d+(\\.\\d*)?)\\s*([kmgKMG])?");
+
+ public static double valueOf(String valueString) {
+ Matcher matcher = pattern.matcher(valueString);
+
+ matcher.matches();
+ double value = Double.valueOf(matcher.group(1));
+ String unit = matcher.group(3);
+ if (unit != null) {
+ value *= unitToValue(toLowerCase(unit).charAt(0));
+ }
+ return value;
+ }
+
+ private static double unitToValue(char unit) {
+ final char units[] = {'k', 'm', 'g'};
+ for (int i=0; i<units.length; ++i) {
+ if (units[i] == unit) {
+ return Math.pow(2, 10*(i+1));
+ }
+ }
+
+ throw new RuntimeException("No such unit: '" + unit + "'");
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java
new file mode 100644
index 00000000000..179f6bad0f3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java
@@ -0,0 +1,101 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.api.ConfigServerSpec;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.HostSystem;
+import com.yahoo.vespa.model.admin.*;
+import com.yahoo.vespa.model.filedistribution.FileDistributionConfigProducer;
+import com.yahoo.config.application.api.FileRegistry;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * A base class for admin model builders, to support common functionality across versions.
+ *
+ * @author lulf
+ * @author vegardh
+ * @since 5.12
+ */
+public abstract class DomAdminBuilderBase extends VespaDomBuilder.DomConfigProducerBuilder<Admin> {
+
+ private static final int DEFAULT_INTERVAL = 1; // in minutes
+ private static final String DEFAULT_CLUSTER_NAME = "vespa";
+
+ private final List<ConfigServerSpec> configServerSpecs;
+ private final FileRegistry fileRegistry;
+ protected final boolean multitenant;
+
+ public DomAdminBuilderBase(FileRegistry fileRegistry, boolean multitenant, List<ConfigServerSpec> configServerSpecs) {
+ this.fileRegistry = fileRegistry;
+ this.multitenant = multitenant;
+ this.configServerSpecs = configServerSpecs;
+ }
+
+ protected List<Configserver> getConfigServersFromSpec(AbstractConfigProducer parent) {
+ List<Configserver> configservers = new ArrayList<>();
+ for (ConfigServerSpec spec : configServerSpecs) {
+ HostSystem hostSystem = parent.getHostSystem();
+ HostResource host = new HostResource(Host.createMultitenantHost(hostSystem, spec.getHostName()));
+ hostSystem.addBoundHost(host);
+ Configserver configserver = new Configserver(parent, spec.getHostName());
+ configserver.setHostResource(host);
+ configserver.setBasePort(configserver.getWantedPort());
+ configserver.initService();
+ configservers.add(configserver);
+ }
+ return configservers;
+ }
+
+ @Override
+ protected Admin doBuild(AbstractConfigProducer parent, Element adminE) {
+ Yamas yamas = getYamas(XML.getChild(adminE, "yamas"));
+ Map<String, MetricsConsumer> metricsConsumers = DomMetricBuilderHelper.buildMetricsConsumers(XML.getChild(adminE, "metric-consumers"));
+
+ Admin admin = new Admin(parent, yamas, metricsConsumers, multitenant);
+
+ doBuildAdmin(admin, adminE);
+
+ new ModelConfigProvider(admin);
+
+ FileDistributionOptions fileDistributionOptions = new DomFileDistributionOptionsBuilder().build(XML.getChild(adminE, "filedistribution"));
+ admin.setFileDistribution(new FileDistributionConfigProducer.Builder(fileDistributionOptions).build(parent, fileRegistry));
+ return admin;
+ }
+
+ protected abstract void doBuildAdmin(Admin admin, Element adminE);
+
+ private Yamas getYamas(Element yamasE) {
+ Yamas yamas;
+ if (yamasE == null) {
+ yamas = new Yamas(DEFAULT_CLUSTER_NAME, DEFAULT_INTERVAL);
+ } else {
+ Integer minutes = getMonitoringInterval(yamasE);
+ if (minutes == null) {
+ minutes = DEFAULT_INTERVAL;
+ }
+ yamas = new Yamas(yamasE.getAttribute("systemname"), minutes);
+ }
+ return yamas;
+ }
+
+ private Integer getMonitoringInterval(Element monitoringE) {
+ Integer minutes = null;
+ String seconds = monitoringE.getAttribute("interval").trim();
+ if (!seconds.isEmpty()) {
+ minutes = Integer.parseInt(seconds);
+ minutes = minutes / 60;
+ if (!(minutes == 1 || minutes == 5)) {
+ throw new IllegalArgumentException("The only allowed values for 'interval' attribute in '" + monitoringE.getTagName() +
+ "' element is 60 or 300.");
+ }
+ }
+ return minutes;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2Builder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2Builder.java
new file mode 100644
index 00000000000..c45cbf8f0d2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2Builder.java
@@ -0,0 +1,197 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.ConfigModelUtils;
+import com.yahoo.config.model.api.ConfigServerSpec;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.model.*;
+import com.yahoo.vespa.model.admin.*;
+import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerCluster;
+import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder.DomConfigProducerBuilder;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.config.application.api.FileRegistry;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * Builds the admin model from a V2 admin XML tag.
+ *
+ * @author vegardh
+ */
+public class DomAdminV2Builder extends DomAdminBuilderBase {
+
+ private static final String ATTRIBUTE_CLUSTER_CONTROLLER_STANDALONE_ZK = "standalone-zookeeper";
+
+ public DomAdminV2Builder(FileRegistry fileRegistry, boolean multitenant, List<ConfigServerSpec> configServerSpecs) {
+ super(fileRegistry, multitenant, configServerSpecs);
+ }
+
+ @Override
+ protected void doBuildAdmin(Admin admin, Element adminE) {
+ List<Configserver> configservers = parseConfigservers(admin, adminE);
+ admin.setLogserver(parseLogserver(admin, adminE));
+ admin.addConfigservers(configservers);
+ admin.addSlobroks(getSlobroks(admin, XML.getChild(adminE, "slobroks")));
+ if ( ! admin.multitenant())
+ admin.setClusterControllers(addConfiguredClusterControllers(admin, adminE));
+ }
+
+ private List<Configserver> parseConfigservers(Admin admin, Element adminE) {
+ List<Configserver> configservers;
+ if (multitenant) {
+ configservers = getConfigServersFromSpec(admin);
+ } else {
+ configservers = getConfigServers(admin, adminE);
+ }
+ int count = configservers.size();
+ if (count % 2 == 0) {
+ admin.deployLogger().log(Level.WARNING, "An even number (" + count + ") of config servers have been configured. " +
+ "This is discouraged, see " + "" +
+ ConfigModelUtils.createDocLink("cloudconfig/configuration-server.html#fault-tolerance"));
+ }
+ return configservers;
+ }
+
+ private Logserver parseLogserver(Admin admin, Element adminE) {
+ Element logserverE = XML.getChild(adminE, "logserver");
+ if (logserverE == null) {
+ logserverE = XML.getChild(adminE, "adminserver");
+ }
+ return new LogserverBuilder().build(admin, logserverE);
+ }
+
+ private ContainerCluster addConfiguredClusterControllers(AbstractConfigProducer parent, Element admin) {
+ Element controllersElements = XML.getChild(admin, "cluster-controllers");
+ if (controllersElements == null) return null;
+
+ List<Element> controllers = XML.getChildren(controllersElements, "cluster-controller");
+ if (controllers.isEmpty()) return null;
+
+ boolean standaloneZooKeeper = "true".equals(controllersElements.getAttribute(ATTRIBUTE_CLUSTER_CONTROLLER_STANDALONE_ZK)) || multitenant;
+ if (standaloneZooKeeper) {
+ parent = new ClusterControllerCluster(parent, "standalone");
+ }
+ ContainerCluster cluster = new ContainerCluster(parent, "cluster-controllers", "cluster-controllers");
+ ContainerModelBuilder.addDefaultHandler_legacyBuilder(cluster);
+
+ List<Container> containers = new ArrayList<>();
+
+ for (Element controller : controllers) {
+ ClusterControllerContainer clusterController = new ClusterControllerBuilder(containers.size(), standaloneZooKeeper).build(cluster, controller);
+ containers.add(clusterController);
+ }
+
+ cluster.addContainers(containers);
+ return cluster;
+ }
+
+ // Extra stupid because configservers tag is voluntary
+ private List<Configserver> getConfigServers(AbstractConfigProducer parent, Element adminE) {
+ SimpleConfigProducer configServers = new SimpleConfigProducer(parent, "configservers");
+ List<Configserver> cfgs = new ArrayList<>();
+ Element configserversE = XML.getChild(adminE, "configservers");
+ if (configserversE == null) {
+ Element configserverE = XML.getChild(adminE, "configserver");
+ if (configserverE == null) {
+ configserverE = XML.getChild(adminE, "adminserver");
+ } else {
+ parent.deployLogger().log(LogLevel.INFO, "Specifying configserver without parent element configservers in services.xml is deprecated");
+ }
+ Configserver cfgs0 = new ConfigserverBuilder(0).build(configServers, configserverE);
+ cfgs0.setProp("index", 0);
+ cfgs.add(cfgs0);
+ return cfgs;
+ }
+ // configservers tag in use
+ int i = 0;
+ for (Element configserverE : XML.getChildren(configserversE, "configserver")) {
+ Configserver cfgsrv = new ConfigserverBuilder(i).build(configServers, configserverE);
+ cfgsrv.setProp("index", i);
+ cfgs.add(cfgsrv);
+ i++;
+ }
+ return cfgs;
+ }
+
+ private List<Slobrok> getSlobroks(AbstractConfigProducer parent, Element slobroksE) {
+ List<Slobrok> slobs = new ArrayList<>();
+ if (slobroksE != null) {
+ slobs = getExplicitSlobrokSetup(parent, slobroksE);
+ }
+ return slobs;
+ }
+
+ private List<Slobrok> getExplicitSlobrokSetup(AbstractConfigProducer parent, Element slobroksE) {
+ List<Slobrok> slobs = new ArrayList<>();
+ List<Element> slobsE = XML.getChildren(slobroksE, "slobrok");
+ int i = 0;
+ for (Element e : slobsE) {
+ Slobrok slob = new SlobrokBuilder(i).build(parent, e);
+ slobs.add(slob);
+ i++;
+ }
+ return slobs;
+ }
+
+ private static class LogserverBuilder extends DomConfigProducerBuilder<Logserver> {
+ public LogserverBuilder() {
+ }
+
+ @Override
+ protected Logserver doBuild(AbstractConfigProducer parent, Element producerSpec) {
+ return new Logserver(parent);
+ }
+ }
+
+ private static class ConfigserverBuilder extends DomConfigProducerBuilder<Configserver> {
+ int i;
+
+ public ConfigserverBuilder(int i) {
+ this.i = i;
+ }
+
+ @Override
+ protected Configserver doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new Configserver(parent, "configserver." + i);
+ }
+ }
+
+ private static class SlobrokBuilder extends DomConfigProducerBuilder<Slobrok> {
+ int i;
+
+ public SlobrokBuilder(int i) {
+ this.i = i;
+ }
+
+ @Override
+ protected Slobrok doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new Slobrok(parent, i);
+ }
+ }
+
+ private static class ClusterControllerBuilder extends DomConfigProducerBuilder<ClusterControllerContainer> {
+ int i;
+ boolean runStandaloneZooKeeper;
+
+ public ClusterControllerBuilder(int i, boolean runStandaloneZooKeeper) {
+ this.i = i;
+ this.runStandaloneZooKeeper = runStandaloneZooKeeper;
+ }
+
+ @Override
+ protected ClusterControllerContainer doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new ClusterControllerContainer(parent, i, runStandaloneZooKeeper);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java
new file mode 100644
index 00000000000..1144ea775ab
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV4Builder.java
@@ -0,0 +1,138 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.api.ConfigServerSpec;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.HostSystem;
+import com.yahoo.vespa.model.admin.*;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerModel;
+
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Builds the admin model from a version 4 XML tag, or as a default when an admin 3 tag or no admin tag is used.
+ *
+ * @author bratseth
+ */
+public class DomAdminV4Builder extends DomAdminBuilderBase {
+
+ private final Collection<ContainerModel> containerModels;
+ private final DeployLogger deployLogger;
+
+ public DomAdminV4Builder(ConfigModelContext modelContext, boolean multitenant, List<ConfigServerSpec> configServerSpecs, Collection<ContainerModel> containerModels) {
+ super(modelContext.getDeployState().getFileRegistry(), multitenant, configServerSpecs);
+ this.containerModels = containerModels;
+ this.deployLogger = modelContext.getDeployLogger();
+ }
+
+ @Override
+ protected void doBuildAdmin(Admin admin, Element w3cAdminElement) {
+ ModelElement adminElement = new ModelElement(w3cAdminElement);
+ admin.addConfigservers(getConfigServersFromSpec(admin));
+
+ // Note: These two elements only exists in admin version 4.0
+ // This build handles admin version 3.0 by ignoring its content (as the content is not useful)
+ Optional<NodesSpecification> requestedSlobroks = NodesSpecification.optionalDedicatedFromParent(adminElement.getChild("slobroks"));
+ Optional<NodesSpecification> requestedLogservers = NodesSpecification.optionalDedicatedFromParent(adminElement.getChild("logservers"));
+ assignSlobroks(requestedSlobroks.orElse(NodesSpecification.nonDedicated(3)), admin);
+ assignLogserver(requestedLogservers.orElse(NodesSpecification.nonDedicated(1)), admin);
+ }
+
+ private void assignSlobroks(NodesSpecification nodesSpecification, Admin admin) {
+ if (nodesSpecification.isDedicated()) {
+ createSlobroks(admin, allocateHosts(admin.getHostSystem(), "slobroks", nodesSpecification));
+ }
+ else {
+ createSlobroks(admin, pickContainerHosts(nodesSpecification.count()));
+ }
+ }
+
+ private void assignLogserver(NodesSpecification nodesSpecification, Admin admin) {
+ if (nodesSpecification.count() > 1) throw new IllegalArgumentException("You can only request a single log server");
+
+ if (nodesSpecification.isDedicated()) {
+ createLogserver(admin, allocateHosts(admin.getHostSystem(), "logserver", nodesSpecification));
+ }
+ else {
+ if (containerModels.iterator().hasNext())
+ createLogserver(admin, sortedContainerHostsFrom(containerModels.iterator().next(), nodesSpecification.count(), false));
+ }
+ }
+
+ private Collection<HostResource> allocateHosts(HostSystem hostSystem, String clusterId, NodesSpecification nodesSpecification) {
+ return nodesSpecification.provision(hostSystem, ClusterSpec.Type.admin, ClusterSpec.Id.from(clusterId), Optional.empty(), deployLogger).keySet();
+ }
+
+ /**
+ * Returns a list of container hosts to use for an auxiliary cluster.
+ * The list returns the same nodes on each invocation given the same available nodes.
+ *
+ * @param count the desired number of nodes. More nodes may be returned to ensure a smooth transition
+ * on topology changes, and less nodes may be returned if fewer are available
+ */
+ private List<HostResource> pickContainerHosts(int count) {
+ // Pick from all container clusters to make sure we don't lose all nodes at once if some clusters are removed.
+ // This will overshoot the desired size (due to ceil and picking at least one node per cluster).
+ List<HostResource> picked = new ArrayList<>();
+ for (ContainerModel containerModel : containerModels)
+ picked.addAll(pickContainerHostsFrom(containerModel,
+ (int)Math.max(1, Math.ceil((double)count/containerModels.size()))));
+ return picked;
+ }
+
+ private List<HostResource> pickContainerHostsFrom(ContainerModel model, int count) {
+ boolean retired = true;
+ List<HostResource> picked = sortedContainerHostsFrom(model, count, !retired);
+
+ // if we can return multiple hosts, include retired nodes which would have been picked before
+ // (probably - assuming all previous nodes were retired, which is always true for a single cluster
+ // at the moment (Sept 2015)) // to ensure a smoother transition between the old and new topology
+ // by including both new and old nodes during the retirement period
+ picked.addAll(sortedContainerHostsFrom(model, count, retired));
+
+ return picked;
+ }
+
+ /** Returns the count first containers in the current model having isRetired set to the given value */
+ private List<HostResource> sortedContainerHostsFrom(ContainerModel model, int count, boolean retired) {
+ List<HostResource> hosts = new ArrayList<>();
+ for (Container container : model.getCluster().getContainers())
+ if (retired == container.isRetired())
+ hosts.add(container.getHostResource());
+ Collections.sort(hosts);
+ return hosts.subList(0, Math.min(count, hosts.size()));
+ }
+
+ private void createLogserver(Admin admin, Collection<HostResource> hosts) {
+ if (hosts.isEmpty()) return; // No log server can be created (and none is needed)
+ Logserver logserver = new Logserver(admin);
+ logserver.setHostResource(hosts.iterator().next());
+ admin.setLogserver(logserver);
+ logserver.initService();
+ }
+
+ private void createSlobroks(Admin admin, Collection<HostResource> hosts) {
+ if (hosts.isEmpty()) return; // No slobroks can be created (and none are needed)
+ List<Slobrok> slobroks = new ArrayList<>();
+ int index = 0;
+ for (HostResource host : hosts) {
+ Slobrok slobrok = new Slobrok(admin, index++);
+ slobrok.setHostResource(host);
+ slobroks.add(slobrok);
+ slobrok.initService();
+ }
+ admin.addSlobroks(slobroks);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientProviderBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientProviderBuilder.java
new file mode 100644
index 00000000000..56415c7232d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientProviderBuilder.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.Handler;
+import org.w3c.dom.Element;
+
+/**
+ * @author gjoranv
+ * @since 5.1.6
+ */
+public class DomClientProviderBuilder extends DomHandlerBuilder {
+
+ @Override
+ protected Handler doBuild(AbstractConfigProducer ancestor, Element clientElement) {
+ Handler<? super Component<?, ?>> client = getHandler(clientElement);
+
+ for (Element binding : XML.getChildren(clientElement, "binding"))
+ client.addClientBindings(XML.getValue(binding));
+
+ for (Element serverBinding : XML.getChildren(clientElement, "serverBinding"))
+ client.addServerBindings(XML.getValue(serverBinding));
+
+ DomComponentBuilder.addChildren(ancestor, clientElement, client);
+
+ return client;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientsBuilder.java
new file mode 100644
index 00000000000..0e0c94abae1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomClientsBuilder.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.vespa.model.clients.*;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Builds the Clients plugin
+ *
+ * @author musum
+ */
+public class DomClientsBuilder extends LegacyConfigModelBuilder<Clients> {
+
+ public DomClientsBuilder() {
+ super(Clients.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return Arrays.asList(ConfigModelId.fromNameAndVersion("clients", "2.0"));
+ }
+
+ @Override
+ public void doBuild(Clients clients, Element clientsE, ConfigModelContext modelContext) {
+ String version = clientsE.getAttribute("version");
+ if (version.startsWith("2.")) {
+ DomV20ClientsBuilder parser = new DomV20ClientsBuilder(clients, version);
+ parser.build(clientsE);
+ } else {
+ throw new IllegalArgumentException("Version '" + version + "' of 'clients' not supported.");
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilder.java
new file mode 100644
index 00000000000..17a59bd7590
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilder.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.xml.BundleInstantiationSpecificationBuilder;
+import org.w3c.dom.Element;
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ */
+public class DomComponentBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Component> {
+
+ public static final String elementName = "component" ;
+
+ private final ComponentId namespace;
+
+ public DomComponentBuilder() {
+ this(null);
+ }
+
+ public DomComponentBuilder(ComponentId namespace) {
+ this.namespace = namespace;
+ }
+
+ protected Component doBuild(AbstractConfigProducer ancestor, Element spec) {
+ Component component = buildComponent(spec);
+ addChildren(ancestor, spec, component);
+ return component;
+ }
+
+ private Component buildComponent(Element spec) {
+ BundleInstantiationSpecification bundleSpec =
+ BundleInstantiationSpecificationBuilder.build(spec, false).nestInNamespace(namespace);
+
+ return new Component<Component<?, ?>, ComponentModel>(new ComponentModel(bundleSpec));
+ }
+
+ public static void addChildren(AbstractConfigProducer ancestor, Element componentNode, Component<? super Component<?, ?>, ?> component) {
+ for (Element childNode : XML.getChildren(componentNode, elementName)) {
+ addAndInjectChild(ancestor, component, childNode);
+ }
+ }
+
+ private static void addAndInjectChild(AbstractConfigProducer ancestor, Component<? super Component<?, ?>, ?> component, Element childNode) {
+ Component<?, ?> child = new DomComponentBuilder(component.getComponentId()).build(ancestor, childNode);
+ component.addComponent(child);
+ component.inject(child);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilder.java
new file mode 100644
index 00000000000..a2478f80397
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilder.java
@@ -0,0 +1,243 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.collections.Tuple2;
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.codegen.CNode;
+import com.yahoo.log.LogLevel;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.config.*;
+
+import com.yahoo.vespa.config.util.ConfigUtils;
+import org.w3c.dom.Element;
+
+import java.util.*;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * Builder that transforms xml config to a slime tree representation of the config. The root element of the xml config
+ * must be named 'config' and have a 'name' attribute that matches the name of the {@link ConfigDefinition}. The values
+ * are not validated against their types. That task is moved to the builders.
+ *
+ * @author lulf
+ */
+public class DomConfigPayloadBuilder {
+
+ private static final Logger log = Logger.getLogger(DomConfigPayloadBuilder.class.getPackage().toString());
+
+ private static final Pattern namePattern = ConfigDefinition.namePattern;
+ private static final Pattern namespacePattern = ConfigDefinition.namespacePattern;
+
+ private final ConfigDefinition configDefinition;
+
+ public DomConfigPayloadBuilder(ConfigDefinition configDefinition) {
+ this.configDefinition = configDefinition;
+ }
+
+ /**
+ * Builds a {@link ConfigPayloadBuilder} representing the input 'config' xml element.
+ *
+ * @param configE The 'config' xml element
+ * @param issuedWarnings a list that will be populated with issued warnings when building the config payload
+ * @return a new payload builder built from xml.
+ */
+ public ConfigPayloadBuilder build(Element configE, List<String> issuedWarnings) {
+ parseConfigName(configE);
+
+ ConfigPayloadBuilder payloadBuilder = new ConfigPayloadBuilder(configDefinition, issuedWarnings);
+ for (Element child : XML.getChildren(configE)) {
+ parseElement(child, payloadBuilder, null);
+ }
+ return payloadBuilder;
+ }
+
+ public static ConfigDefinitionKey parseConfigName(Element configE) {
+ if (!configE.getNodeName().equals("config")) {
+ throw new ConfigurationRuntimeException("The root element must be 'config', but was '"
+ + configE.getNodeName() + "'.");
+ }
+ if (!configE.hasAttribute("name")) {
+ throw new ConfigurationRuntimeException
+ ("The 'config' element must have a 'name' attribute that matches the name of the config definition.");
+ }
+
+ String xmlName = configE.getAttribute("name");
+ final boolean xmlNamespaceAttributeExists = configE.hasAttribute("namespace");
+
+ String xmlNamespace = null;
+ // If name contains dots, rewrite to name and namespace
+ if (xmlName.contains(".")) {
+ Tuple2<String, String> t = ConfigUtils.getNameAndNamespaceFromString(xmlName);
+ xmlName = t.first;
+ xmlNamespace = t.second;
+ } else {
+ if (!xmlNamespaceAttributeExists) {
+ log.log(LogLevel.WARNING, "No namespace in 'config name=" + xmlName + "', please specify one");
+ }
+ }
+
+ if (!validName(xmlName)) {
+ throw new ConfigurationRuntimeException("The config name '" + xmlName +
+ "' contains illegal characters. Only names with the pattern " + namePattern.toString() + " are legal.");
+ }
+
+ if (xmlNamespace == null) {
+ xmlNamespace = configE.getAttribute("namespace");
+ if (xmlNamespace == null || xmlNamespace.isEmpty()) {
+ xmlNamespace = CNode.DEFAULT_NAMESPACE;
+ }
+ }
+ if (!validNamespace(xmlNamespace)) {
+ throw new ConfigurationRuntimeException("The config namespace '" + xmlNamespace +
+ "' contains illegal characters. Only namespaces with the pattern " + namespacePattern.toString() + " are legal.");
+ }
+ return new ConfigDefinitionKey(xmlName, xmlNamespace);
+ }
+
+ private static boolean validName(String name) {
+ Matcher m = namePattern.matcher(name);
+ return m.matches();
+ }
+
+ private static boolean validNamespace(String namespace) {
+ Matcher m = namespacePattern.matcher(namespace);
+ return m.matches();
+ }
+
+ private String extractName(Element element) {
+ String initial = element.getNodeName();
+ if (initial.indexOf('-') < 0) {
+ return initial;
+ }
+ StringBuilder buf = new StringBuilder();
+ boolean upcase = false;
+ for (char ch : initial.toCharArray()) {
+ if (ch == '-') {
+ upcase = true;
+ } else if (upcase && ch >= 'a' && ch <= 'z') {
+ buf.append((char)('A' + ch - 'a'));
+ upcase = false;
+ } else {
+ buf.append(ch);
+ upcase = false;
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Parse leaf value in an xml tree
+ */
+ private void parseLeaf(Element element, ConfigPayloadBuilder payloadBuilder, String parentName) {
+ String name = extractName(element);
+ String value = XML.getValue(element);
+ if (value == null) {
+ throw new ConfigurationRuntimeException("Element '" + name + "' must have either children or a value");
+ }
+
+
+ if (element.hasAttribute("index")) {
+ // Check for legacy (pre Vespa 6) usage
+ throw new IllegalArgumentException("The 'index' attribute on config elements is not supported - use <item>");
+ } else if (element.hasAttribute("operation")) {
+ // leaf array, currently the only supported operation is 'append'
+ verifyLegalOperation(element);
+ ConfigPayloadBuilder.Array a = payloadBuilder.getArray(name);
+ a.append(value);
+ } else if ("item".equals(name)) {
+ if (parentName == null)
+ throw new ConfigurationRuntimeException("<item> is a reserved keyword for array and map elements");
+ if (element.hasAttribute("key")) {
+ payloadBuilder.getMap(parentName).put(element.getAttribute("key"), value);
+ } else {
+ payloadBuilder.getArray(parentName).append(value);
+ }
+ } else {
+ // leaf scalar, e.g. <intVal>3</intVal>
+ payloadBuilder.setField(name, value);
+ }
+ }
+
+ private void parseComplex(Element element, List<Element> children, ConfigPayloadBuilder payloadBuilder, String parentName) {
+ String name = extractName(element);
+ // Inner value
+ if (element.hasAttribute("index")) {
+ // Check for legacy (pre Vespa 6) usage
+ throw new IllegalArgumentException("The 'index' attribute on config elements is not supported - use <item>");
+ } else if (element.hasAttribute("operation")) {
+ // inner array, currently the only supported operation is 'append'
+ verifyLegalOperation(element);
+ ConfigPayloadBuilder childPayloadBuilder = payloadBuilder.getArray(name).append();
+ //Cursor array = node.setArray(name);
+ for (Element child : children) {
+ //Cursor struct = array.addObject();
+ parseElement(child, childPayloadBuilder, name);
+ }
+ } else if ("item".equals(name)) {
+ // Reserved item means array/map element as struct
+ if (element.hasAttribute("key")) {
+ ConfigPayloadBuilder childPayloadBuilder = payloadBuilder.getMap(parentName).get(element.getAttribute("key"));
+ for (Element child : children) {
+ parseElement(child, childPayloadBuilder, parentName);
+ }
+ } else {
+ ConfigPayloadBuilder.Array array = payloadBuilder.getArray(parentName);
+ ConfigPayloadBuilder childPayloadBuilder = array.append();
+ for (Element child : children) {
+ parseElement(child, childPayloadBuilder, parentName);
+ }
+ }
+ } else {
+ int numMatching = 0;
+ for (Element child : children) {
+ numMatching += ("item".equals(child.getTagName())) ? 1 : 0;
+ }
+
+ if (numMatching == 0) {
+ // struct, e.g. <basicStruct>
+ ConfigPayloadBuilder p = payloadBuilder.getObject(name);
+ //Cursor struct = node.setObject(name);
+ for (Element child : children)
+ parseElement(child, p, name);
+ } else if (numMatching == children.size()) {
+ // Array with <item elements>
+ for (Element child : children) {
+ parseElement(child, payloadBuilder, name);
+ }
+ } else {
+ throw new ConfigurationRuntimeException("<item> is a reserved keyword for array and map elements");
+ }
+ }
+ }
+
+ /**
+ * Adds the values and children (recursively) in the given xml element to the given {@link ConfigPayloadBuilder}.
+ * @param currElem The element representing a config parameter.
+ * @param payloadBuilder The builder to use when adding elements.
+ */
+ private void parseElement(Element currElem, ConfigPayloadBuilder payloadBuilder, String parentName) {
+ List<Element> children = XML.getChildren(currElem);
+ try {
+ if (children.isEmpty()) {
+ parseLeaf(currElem, payloadBuilder, parentName);
+ } else {
+ parseComplex(currElem, children, payloadBuilder, parentName);
+ }
+ } catch (Exception exception) {
+ throw new ConfigurationRuntimeException("Error parsing element at " + XML.getNodePath(currElem, " > ") + ": " +
+ Exceptions.toMessageString(exception));
+ }
+ }
+
+ private void verifyLegalOperation(Element currElem) {
+ String operation = currElem.getAttribute("operation");
+ if (! operation.equalsIgnoreCase("append"))
+ throw new ConfigurationRuntimeException("The only supported array operation is 'append', got '"
+ + operation + "' at XML node '" + XML.getNodePath(currElem, " > ") + "'.");
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContainerClusterBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContainerClusterBuilder.java
new file mode 100644
index 00000000000..fe339835c78
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContainerClusterBuilder.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.Handler;
+import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author gjoranv
+ */
+public abstract class DomContainerClusterBuilder<CLUSTER extends ContainerCluster>
+ extends VespaDomBuilder.DomConfigProducerBuilder<CLUSTER> {
+
+ protected final Element outerChainsElem;
+
+ public DomContainerClusterBuilder(Element outerChainsElem) {
+ this.outerChainsElem = outerChainsElem;
+ }
+
+ protected void buildAndAddUserConfiguredComponents(ContainerCluster cluster, Element spec) {
+ buildAndAddConfiguredHandlers(cluster, spec);
+ buildAndAddClientProviders(cluster, spec);
+ buildAndAddServerProviders(cluster, spec);
+ buildAndAddGenericComponents(cluster, spec);
+ buildAndAddRenderers(cluster, spec);
+ buildAndAddFilters(cluster, spec);
+ }
+
+ public void addSpecialHandlers(ContainerCluster cluster) {
+ ContainerModelBuilder.addDefaultHandler_legacyBuilder(cluster);
+ }
+
+ private void buildAndAddConfiguredHandlers(ContainerCluster cluster, Element spec) {
+ List<Handler> handlers = buildConfiguredHandlers(new DomHandlerBuilder(true), cluster, spec, "handler");
+
+ for (Handler handler : handlers) {
+ // TODO: hack to avoid adding a simple Handler for an explicitly declared SearchHandler
+ if (handler.getClassId().getName().equals("com.yahoo.search.handler.SearchHandler")) {
+ final ProcessingHandler<SearchChains> searchHandler = new ProcessingHandler<>(
+ cluster.getSearch().getChains(), "com.yahoo.search.handler.SearchHandler");
+ searchHandler.addServerBindings("http://*/search/*");
+ cluster.addComponent(searchHandler);
+ } else
+ cluster.addComponent(handler);
+ }
+ }
+
+ private void buildAndAddClientProviders(ContainerCluster cluster, Element spec) {
+ List<Handler> clients = buildConfiguredHandlers(new DomClientProviderBuilder(), cluster, spec, "client");
+
+ for (Handler client : clients) {
+ cluster.addComponent(client);
+ }
+ }
+
+ private void buildAndAddServerProviders(ContainerCluster cluster, Element spec) {
+ ContainerModelBuilder.addConfiguredComponents(cluster, spec, "server");
+ }
+
+ private void buildAndAddGenericComponents(ContainerCluster cluster, Element spec) {
+ ContainerModelBuilder.addConfiguredComponents(cluster, spec, DomComponentBuilder.elementName);
+ }
+
+ private void buildAndAddFilters(ContainerCluster cluster, Element spec) {
+ for (Component component : buildConfiguredFilters(cluster, spec, "filter")) {
+ cluster.addComponent(component);
+ }
+ }
+
+ private List<Component> buildConfiguredFilters(AbstractConfigProducer ancestor,
+ Element spec,
+ String componentName) {
+ List<Component> components = new ArrayList<>();
+
+ for (Element node : XML.getChildren(spec, componentName)) {
+ components.add(new DomFilterBuilder().build(ancestor, node));
+ }
+ return components;
+ }
+
+ private List<Handler> buildConfiguredHandlers(DomHandlerBuilder builder,
+ AbstractConfigProducer ancestor,
+ Element spec,
+ String componentName) {
+ List<Handler> handlers = new ArrayList<>();
+
+ for (Element node : XML.getChildren(spec, componentName)) {
+ handlers.add(builder.build(ancestor, node));
+ }
+ return handlers;
+ }
+
+ protected void buildAndAddRenderers(ContainerCluster cluster, Element spec) {
+ ContainerModelBuilder.addConfiguredComponents(cluster, spec, "renderer");
+ }
+
+ protected void buildAndAddProcessingRenderers(ContainerCluster cluster, Element spec) {
+ ContainerModelBuilder.addConfiguredComponents(cluster, spec, "renderer");
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilder.java
new file mode 100644
index 00000000000..930e7105c2f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilder.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author balder
+ */
+public class DomContentBuilder extends ConfigModelBuilder<Content> {
+
+ public static final List<ConfigModelId> configModelIds = Collections.singletonList(ConfigModelId.fromName("content"));
+
+ public DomContentBuilder() {
+ super(Content.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return configModelIds;
+ }
+
+ @Override
+ public void doBuild(Content content, Element xml, ConfigModelContext modelContext) {
+ Admin admin = content.adminModel() != null ? content.adminModel().getAdmin() : null; // This is null in tests only
+ ContentCluster cluster = new ContentCluster.Builder(admin, modelContext.getDeployLogger()).build(modelContext.getParentProducer(), xml);
+ content.setCluster(cluster, modelContext);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFileDistributionOptionsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFileDistributionOptionsBuilder.java
new file mode 100644
index 00000000000..434c57fdcb3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFileDistributionOptionsBuilder.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.admin.FileDistributionOptions;
+import org.w3c.dom.Element;
+
+/**
+ * Builds a file distribution options.
+ * @author tonytv
+ */
+public class DomFileDistributionOptionsBuilder {
+ private static void throwExceptionForElementInFileDistribution(String subElement, String reason) {
+ throw new RuntimeException("In element '" + subElement + "' contained in 'filedistribution': " + reason);
+ }
+
+ private static void callSetter(FileDistributionOptions options, String name, BinaryScaledAmount amount) {
+ try {
+ options.getClass().getMethod(name, BinaryScaledAmountParser.class).invoke(options, amount);
+ } catch (IllegalArgumentException e) {
+ throwExceptionForElementInFileDistribution(name, e.getMessage());
+ }
+ catch (Exception e) {
+ if (e instanceof RuntimeException)
+ throw (RuntimeException)e;
+ else
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void setIfPresent(FileDistributionOptions options, String name, Element fileDistributionElement) {
+ try {
+ Element optionElement = XML.getChild(fileDistributionElement, name);
+ if (optionElement != null) {
+ String valueString = XML.getValue(optionElement);
+ BinaryScaledAmount amount = BinaryScaledAmountParser.parse(valueString);
+ callSetter(options, name, amount);
+ }
+ } catch (NumberFormatException e) {
+ throwExceptionForElementInFileDistribution(name, "Expected a valid number. (Message = " + e.getMessage() + ").");
+ }
+ }
+
+ public FileDistributionOptions build(Element fileDistributionElement) {
+ FileDistributionOptions options = FileDistributionOptions.defaultOptions();
+ if (fileDistributionElement != null) {
+ setIfPresent(options, "uploadbitrate", fileDistributionElement);
+ setIfPresent(options, "downloadbitrate", fileDistributionElement);
+ }
+ return options;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFilterBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFilterBuilder.java
new file mode 100644
index 00000000000..741b6f99944
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomFilterBuilder.java
@@ -0,0 +1,18 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.HttpFilter;
+import com.yahoo.vespa.model.container.xml.BundleInstantiationSpecificationBuilder;
+import org.w3c.dom.Element;
+
+/**
+ * @author tonytv
+ */
+public class DomFilterBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Component> {
+ @Override
+ protected Component doBuild(AbstractConfigProducer ancestor, Element element) {
+ return new HttpFilter(BundleInstantiationSpecificationBuilder.build(element, false));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomHandlerBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomHandlerBuilder.java
new file mode 100644
index 00000000000..39a3ab553d2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomHandlerBuilder.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.Handler;
+import com.yahoo.vespa.model.container.xml.BundleInstantiationSpecificationBuilder;
+import org.w3c.dom.Element;
+
+/**
+ * @author gjoranv
+ * @since 5.1.6
+ */
+public class DomHandlerBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Handler> {
+ private final boolean legacyMode;
+
+ public DomHandlerBuilder(boolean legacyMode) {
+ this.legacyMode = legacyMode;
+ }
+
+ public DomHandlerBuilder() {
+ this(false);
+ }
+
+ @Override
+ protected Handler doBuild(AbstractConfigProducer ancestor, Element handlerElement) {
+ Handler<? super Component<?, ?>> handler = getHandler(handlerElement);
+
+ for (Element binding : XML.getChildren(handlerElement, "binding"))
+ handler.addServerBindings(XML.getValue(binding));
+
+ for (Element clientBinding : XML.getChildren(handlerElement, "clientBinding"))
+ handler.addClientBindings(XML.getValue(clientBinding));
+
+ DomComponentBuilder.addChildren(ancestor, handlerElement, handler);
+
+ return handler;
+ }
+
+ protected Handler<? super Component<?, ?>> getHandler(Element handlerElement) {
+ BundleInstantiationSpecification bundleSpec = BundleInstantiationSpecificationBuilder.build(handlerElement, legacyMode);
+ return new Handler<>(
+ new ComponentModel(bundleSpec));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomMetricBuilderHelper.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomMetricBuilderHelper.java
new file mode 100644
index 00000000000..49d475ec057
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomMetricBuilderHelper.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.admin.Metric;
+import com.yahoo.vespa.model.admin.MetricsConsumer;
+import org.w3c.dom.Element;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class for parsing yamasmetric config.
+ *
+ * @author trygve
+ * @since 5.1
+ */
+public class DomMetricBuilderHelper {
+
+
+ /**
+ * Build metricConsumer config
+ *
+ * @param spec xml element
+ * @return a map from metric name to a {@link MetricsConsumer}
+ */
+ protected static Map<String, MetricsConsumer> buildMetricsConsumers(Element spec) {
+ Map<String, MetricsConsumer> metricsConsumers = new LinkedHashMap<>();
+ List<Element> consumersElem = XML.getChildren(spec, "consumer");
+ for (Element consumer : consumersElem) {
+ String consumerName = consumer.getAttribute("name");
+ Map<String, Metric> metrics = new LinkedHashMap<>();
+ List<Element> metricsEl = XML.getChildren(consumer, "metric");
+ if (metricsEl != null) {
+ for (Element metric : metricsEl) {
+ String metricName = metric.getAttribute("name");
+ String outputName = metric.getAttribute("output-name");
+ metrics.put(metricName, new Metric(metricName, outputName));
+ }
+ }
+ MetricsConsumer metricsConsumer = new MetricsConsumer(consumerName, metrics);
+ metricsConsumers.put(consumerName, metricsConsumer);
+ }
+ return metricsConsumers;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomRoutingBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomRoutingBuilder.java
new file mode 100644
index 00000000000..dad00f529fb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomRoutingBuilder.java
@@ -0,0 +1,138 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.application.Xml;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.messagebus.routing.*;
+import com.yahoo.text.XML;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.vespa.model.routing.Routing;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Builds the Routing plugin
+ *
+ * @author vegardh
+ */
+public class DomRoutingBuilder extends ConfigModelBuilder<Routing> {
+
+ public DomRoutingBuilder() {
+ super(Routing.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return Arrays.asList(ConfigModelId.fromName("routing"));
+ }
+
+ // Overrides ConfigModelBuilder.
+ @Override
+ public void doBuild(Routing plugin, Element spec, ConfigModelContext modelContext) {
+ ApplicationSpec app = null;
+ RoutingSpec routing = null;
+ if (spec != null) {
+ app = new ApplicationSpec();
+ for (Element node : Xml.mergeElems(spec, "services", modelContext.getApplicationPackage(), ApplicationPackage.ROUTINGTABLES_DIR)) {
+ addServices(app, node);
+ }
+ routing = new RoutingSpec();
+ for (Element node : Xml.mergeElems(spec, "routingtable", modelContext.getApplicationPackage(), ApplicationPackage.ROUTINGTABLES_DIR)) {
+ addRoutingTable(routing, node);
+ }
+ }
+ plugin.setExplicitApplicationSpec(app);
+ plugin.setExplicitRoutingSpec(routing);
+ }
+
+ /**
+ * This function updates the given application with the data contained in the given xml element.
+ *
+ * @param app The application to update.
+ * @param element The element to base the services on.
+ */
+ private static void addServices(ApplicationSpec app, Element element) {
+ String protocol = element.getAttribute("protocol");
+ for (Element node : XML.getChildren(element, "service")) {
+ app.addService(protocol, node.getAttribute("name"));
+ }
+ }
+
+ /**
+ * This function updates the given routing spec with the data contained in the given xml element.
+ *
+ * @param routing The routing spec to update.
+ * @param element The element to base the route config on.
+ */
+ private static void addRoutingTable(RoutingSpec routing, Element element) {
+ boolean verify = element.hasAttribute("verify") ? Boolean.valueOf(element.getAttribute("verify")) : true;
+ RoutingTableSpec table = new RoutingTableSpec(element.getAttribute("protocol"), verify);
+
+ NodeList children = element.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node node = children.item(i);
+ if ("hop".equals(node.getNodeName())) {
+ table.addHop(createHopSpec((Element)node));
+ } else if ("route".equals(node.getNodeName())) {
+ table.addRoute(createRouteSpec((Element)node));
+ }
+ }
+
+ routing.addTable(table);
+ }
+
+ /**
+ * This function creates a route from the content of the given xml element.
+ *
+ * @param element The element to base the route config on.
+ * @return The corresponding route spec.
+ */
+ private static RouteSpec createRouteSpec(Element element) {
+ boolean verify = element.hasAttribute("verify") ? Boolean.valueOf(element.getAttribute("verify")) : true;
+ RouteSpec route = new RouteSpec(element.getAttribute("name"), verify);
+ String hops = element.getAttribute("hops");
+ int from = 0;
+ for (int to = 0, depth = 0, len = hops.length(); to < len; ++to) {
+ if (hops.charAt(to) == '[') {
+ ++depth;
+ } else if (hops.charAt(to) == ']') {
+ --depth;
+ } else if (hops.charAt(to) == ' ' && depth == 0) {
+ if (to > from) {
+ route.addHop(hops.substring(from, to));
+ }
+ from = to + 1;
+ }
+ }
+ if (from < hops.length()) {
+ route.addHop(hops.substring(from));
+ }
+ return route;
+ }
+
+ /**
+ * This function creates a hop from the content of the given xml element.
+ *
+ * @param element The element to base the hop config on.
+ * @return The corresponding hop spec.
+ */
+ private static HopSpec createHopSpec(Element element) {
+ boolean verify = element.hasAttribute("verify") ? Boolean.valueOf(element.getAttribute("verify")) : true;
+ HopSpec hop = new HopSpec(element.getAttribute("name"), element.getAttribute("selector"), verify);
+ if (Boolean.valueOf(element.getAttribute("ignore-result"))) {
+ hop.setIgnoreResult(true);
+ }
+ NodeList children = element.getElementsByTagName("recipient");
+ for (int i = 0; i < children.getLength(); i++) {
+ Element node = (Element)children.item(i);
+ hop.addRecipient(node.getAttribute("session"));
+ }
+ return hop;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilder.java
new file mode 100644
index 00000000000..aa2dfd84eb5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilder.java
@@ -0,0 +1,247 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.search.Tuning;
+import org.w3c.dom.Element;
+
+/**
+ * Builder for the tuning config for a search cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class DomSearchTuningBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Tuning> {
+
+ @Override
+ protected Tuning doBuild(AbstractConfigProducer parent, Element spec) {
+ Tuning tuning = new Tuning(parent);
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("dispatch", e)) {
+ handleDispatch(e, tuning);
+ } else if (equals("searchnode", e)) {
+ handleSearchNode(e, tuning);
+ }
+ }
+ return tuning;
+ }
+
+ private static boolean equals(String name, Element e) {
+ return name.equals(e.getNodeName());
+ }
+
+ private static String asString(Element e) {
+ return e.getFirstChild().getNodeValue();
+ }
+
+ private static Long asLong(Element e) {
+ return Long.parseLong(e.getFirstChild().getNodeValue());
+ }
+
+ private static Integer asInt(Element e) {
+ return Integer.parseInt(e.getFirstChild().getNodeValue());
+ }
+
+ private static Double asDouble(Element e) {
+ return Double.parseDouble(e.getFirstChild().getNodeValue());
+ }
+
+ private void handleDispatch(Element spec, Tuning t) {
+ t.dispatch = new Tuning.Dispatch();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("max-hits-per-partition", e)) {
+ t.dispatch.maxHitsPerPartition = asInt(e);
+ }
+ }
+ }
+
+ private void handleSearchNode(Element spec, Tuning t) {
+ t.searchNode = new Tuning.SearchNode();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("requestthreads", e)) {
+ handleRequestThreads(e, t.searchNode);
+ } else if (equals("flushstrategy", e)) {
+ handleFlushStrategy(e, t.searchNode);
+ } else if (equals("resizing", e)) {
+ handleResizing(e, t.searchNode);
+ } else if (equals("index", e)) {
+ handleIndex(e, t.searchNode);
+ } else if (equals("attribute", e)) {
+ handleAttribute(e, t.searchNode);
+ } else if (equals("summary", e)) {
+ handleSummary(e, t.searchNode);
+ }
+ }
+ }
+
+ private void handleRequestThreads(Element spec, Tuning.SearchNode sn) {
+ sn.threads = new Tuning.SearchNode.RequestThreads();
+ Tuning.SearchNode.RequestThreads rt = sn.threads;
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("search", e)) {
+ rt.numSearchThreads = asInt(e);
+ } else if (equals("persearch", e)) {
+ rt.numThreadsPerSearch = asInt(e);
+ } else if (equals("summary", e)) {
+ rt.numSummaryThreads = asInt(e);
+ }
+ }
+ }
+
+ private void handleFlushStrategy(Element spec, Tuning.SearchNode sn) {
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("native", e)) {
+ handleNativeStrategy(e, sn);
+ }
+ }
+ }
+
+ private void handleNativeStrategy(Element spec, Tuning.SearchNode sn) {
+ sn.strategy = new Tuning.SearchNode.FlushStrategy();
+ Tuning.SearchNode.FlushStrategy fs = sn.strategy;
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("total", e)) {
+ for (Element e2 : XML.getChildren(e)) {
+ if (equals("maxmemorygain", e2)) {
+ fs.totalMaxMemoryGain = asLong(e2);
+ } else if (equals("diskbloatfactor", e2)) {
+ fs.totalDiskBloatFactor = asDouble(e2);
+ }
+ }
+ } else if (equals("component", e)) {
+ for (Element e2 : XML.getChildren(e)) {
+ if (equals("maxmemorygain", e2)) {
+ fs.componentMaxMemoryGain = asLong(e2);
+ } else if (equals("diskbloatfactor", e2)) {
+ fs.componentDiskBloatFactor = asDouble(e2);
+ } else if (equals("maxage", e2)) {
+ fs.componentMaxage = asDouble(e2);
+ }
+ }
+ } else if (equals("transactionlog", e)) {
+ for (Element e2 : XML.getChildren(e)) {
+ if (equals("maxentries", e2)) {
+ fs.transactionLogMaxEntries = asLong(e2);
+ } else if (equals("maxsize", e2)) {
+ fs.transactionLogMaxSize = asLong(e2);
+ }
+ }
+ }
+ }
+ }
+
+ private void handleResizing(Element spec, Tuning.SearchNode sn) {
+ sn.resizing = new Tuning.SearchNode.Resizing();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("initialdocumentcount", e)) {
+ sn.resizing.initialDocumentCount = asInt(e);
+ }
+ }
+ }
+
+ private void handleIndex(Element spec, Tuning.SearchNode sn) {
+ sn.index = new Tuning.SearchNode.Index();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("io", e)) {
+ sn.index.io = new Tuning.SearchNode.Index.Io();
+ Tuning.SearchNode.Index.Io io = sn.index.io;
+ for (Element e2 : XML.getChildren(e)) {
+ if (equals("write", e2)) {
+ io.write = Tuning.SearchNode.IoType.fromString(asString(e2));
+ } else if (equals("read", e2)) {
+ io.read = Tuning.SearchNode.IoType.fromString(asString(e2));
+ } else if (equals("search", e2)) {
+ io.search = Tuning.SearchNode.IoType.fromString(asString(e2));
+ }
+ }
+ }
+ }
+ }
+
+ private void handleAttribute(Element spec, Tuning.SearchNode sn) {
+ sn.attribute = new Tuning.SearchNode.Attribute();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("io", e)) {
+ sn.attribute.io = new Tuning.SearchNode.Attribute.Io();
+ for (Element e2 : XML.getChildren(e)) {
+ if (equals("write", e2)) {
+ sn.attribute.io.write = Tuning.SearchNode.IoType.fromString(asString(e2));
+ }
+ }
+ }
+ }
+ }
+
+ private void handleSummary(Element spec, Tuning.SearchNode sn) {
+ sn.summary = new Tuning.SearchNode.Summary();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("io", e)) {
+ sn.summary.io = new Tuning.SearchNode.Summary.Io();
+ for (Element e2 : XML.getChildren(e)) {
+ if (equals("write", e2)) {
+ sn.summary.io.write = Tuning.SearchNode.IoType.fromString(asString(e2));
+ } else if (equals("read", e2)) {
+ sn.summary.io.read = Tuning.SearchNode.IoType.fromString(asString(e2));
+ }
+ }
+ } else if (equals("store", e)) {
+ handleSummaryStore(e, sn.summary);
+ }
+ }
+ }
+
+ private void handleSummaryStore(Element spec, Tuning.SearchNode.Summary s) {
+ s.store = new Tuning.SearchNode.Summary.Store();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("cache", e)) {
+ s.store.cache = new Tuning.SearchNode.Summary.Store.Component();
+ handleSummaryStoreComponent(e, s.store.cache);
+ } else if (equals("logstore", e)) {
+ handleSummaryLogStore(e, s.store);
+ }
+ }
+ }
+
+ private void handleSummaryStoreComponent(Element spec, Tuning.SearchNode.Summary.Store.Component c) {
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("maxsize", e)) {
+ c.maxSize = asLong(e);
+ } else if (equals("maxentries", e)) {
+ c.maxEntries = asLong(e);
+ } else if (equals("initialentries", e)) {
+ c.initialEntries = asLong(e);
+ } else if (equals("compression", e)) {
+ c.compression = new Tuning.SearchNode.Summary.Store.Compression();
+ handleSummaryStoreCompression(e, c.compression);
+ }
+ }
+ }
+
+ private void handleSummaryStoreCompression(Element spec, Tuning.SearchNode.Summary.Store.Compression c) {
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("type", e)) {
+ c.type = Tuning.SearchNode.Summary.Store.Compression.Type.fromString(asString(e));
+ } else if (equals("level", e)) {
+ c.level = asInt(e);
+ }
+ }
+ }
+
+ private void handleSummaryLogStore(Element spec, Tuning.SearchNode.Summary.Store s) {
+ s.logStore = new Tuning.SearchNode.Summary.Store.LogStore();
+ for (Element e : XML.getChildren(spec)) {
+ if (equals("maxfilesize", e)) {
+ s.logStore.maxFileSize = asLong(e);
+ } else if (equals("maxdiskbloatfactor", e)) {
+ s.logStore.maxDiskBloatFactor = asDouble(e);
+ } else if (equals("minfilesizefactor", e)) {
+ s.logStore.minFileSizeFactor = asDouble(e);
+ } else if (equals("numthreads", e)) {
+ s.logStore.numThreads = asInt(e);
+ } else if (equals("chunk", e)) {
+ s.logStore.chunk = new Tuning.SearchNode.Summary.Store.Component(true);
+ handleSummaryStoreComponent(e, s.logStore.chunk);
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomV20ClientsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomV20ClientsBuilder.java
new file mode 100644
index 00000000000..65519637bd5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomV20ClientsBuilder.java
@@ -0,0 +1,576 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.ConfigModelUtils;
+import com.yahoo.vespa.config.content.spooler.SpoolerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.SimpleConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder.DomConfigProducerBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.docproc.DomDocprocChainsBuilder;
+import com.yahoo.vespa.model.clients.Clients;
+import com.yahoo.vespa.model.clients.HttpGatewayOwner;
+import com.yahoo.vespa.model.clients.VespaSpoolMaster;
+import com.yahoo.vespa.model.clients.VespaSpooler;
+import com.yahoo.vespa.model.clients.VespaSpoolerProducer;
+import com.yahoo.vespa.model.clients.VespaSpoolerService;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Handler;
+import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.container.docproc.DocprocChains;
+import com.yahoo.vespa.model.container.search.ContainerHttpGateway;
+import com.yahoo.vespa.model.container.search.ContainerSearch;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChain;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespaclient.config.FeederConfig;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+
+/**
+ * Builds the Clients plugin
+ *
+ * @author vegardh
+ */
+public class DomV20ClientsBuilder {
+
+ public static final String vespaClientBundleSpecification = "vespaclient-container-plugin";
+
+ // The parent docproc plugin to register data with.
+ private final Clients clients;
+
+ DomV20ClientsBuilder(Clients clients, String version) {
+ this.clients = clients;
+ if (!version.equals("2.0")) {
+ throw new IllegalArgumentException("Version '" + version + "' of 'clients' not supported.");
+ }
+ }
+
+ public void build(Element spec) {
+ NodeList children = spec.getElementsByTagName("gateways");
+ if (children.getLength() > 0 && clients.getConfigProducer()!=null)
+ clients.getConfigProducer().deployLogger().log(Level.WARNING, "The 'gateways' element is deprecated, and will be disallowed in a " +
+ "later version of Vespa. Use 'document-api' under 'jdisc' instead, see: " +
+ ConfigModelUtils.createDocLink("reference/services-jdisc.html"));
+ for (int i = 0; i < children.getLength(); i++) {
+ createGateways(clients.getConfigProducer(), (Element) children.item(i), clients);
+ }
+
+ children = spec.getElementsByTagName("spoolers");
+ for (int i = 0; i < children.getLength(); i++) {
+ createSpoolers(clients.getConfigProducer(), (Element) children.item(i), clients);
+ }
+
+ children = spec.getElementsByTagName("load-types");
+ for (int i = 0; i < children.getLength(); i++) {
+ createLoadTypes((Element) children.item(i), clients);
+ }
+ }
+
+ static Boolean getBooleanNodeValue(Node node) {
+ return Boolean.valueOf(node.getFirstChild().getNodeValue());
+ }
+
+ static boolean getHttpFileServerEnabled(Element parentHttpFileServer, Element httpFileServer) {
+ boolean ret=false;
+ if (parentHttpFileServer != null) {
+ for (Element child : XML.getChildren(parentHttpFileServer)) {
+ if ("enabled".equals(child.getNodeName())) {
+ ret = getBooleanNodeValue(child);
+ }
+ }
+ }
+ if (httpFileServer != null) {
+ for (Element child : XML.getChildren(httpFileServer)) {
+ if ("enabled".equals(child.getNodeName())) {
+ ret = getBooleanNodeValue(child);
+ }
+ }
+ }
+ return ret;
+ }
+
+ static String getHttpFileServerRootDir(Element parentHttpFileServer, Element httpFileServer) {
+ String ret="";
+ if (parentHttpFileServer != null) {
+ for (Element child : XML.getChildren(parentHttpFileServer)) {
+ if ("rootdir".equals(child.getNodeName())) {
+ ret = child.getFirstChild().getNodeValue();
+ }
+ }
+ }
+ if (httpFileServer != null) {
+ for (Element child : XML.getChildren(httpFileServer)) {
+ if ("rootdir".equals(child.getNodeName())) {
+ ret = child.getFirstChild().getNodeValue();
+ }
+ }
+ }
+ return ret;
+ }
+
+ private void createLoadTypes(Element element, Clients clients) {
+ for (Element e : XML.getChildren(element, "type")) {
+ String priority = e.getAttribute("default-priority");
+ clients.getLoadTypes().addType(e.getAttribute("name"), priority.length() > 0 ? priority : null);
+ }
+ }
+
+ /**
+ * Creates HttpGateway objects using the given xml Element.
+ *
+ * @param pcp AbstractConfigProducer
+ * @param element The xml Element
+ */
+ private void createGateways(AbstractConfigProducer pcp, Element element, Clients clients) {
+ String jvmArgs = null;
+ if (element.hasAttribute(VespaDomBuilder.JVMARGS_ATTRIB_NAME)) jvmArgs=element.getAttribute(VespaDomBuilder.JVMARGS_ATTRIB_NAME);
+
+ Element gatewaysFeederOptions = findFeederOptions(element);
+
+ HttpGatewayOwner owner = new HttpGatewayOwner(pcp, getFeederConfig(null, gatewaysFeederOptions));
+ ContainerCluster cluster = new ContainerHttpGatewayClusterBuilder().build(owner, element);
+
+ int index = 0;
+ for (Element e : XML.getChildren(element, "gateway")) {
+ ContainerHttpGateway qrs = new ContainerHttpGatewayBuilder(cluster, index).build(cluster, e);
+
+ if ("".equals(qrs.getJvmArgs()) && jvmArgs!=null) qrs.setJvmArgs(jvmArgs);
+ index++;
+ }
+ clients.setContainerHttpGateways(cluster);
+ }
+
+ /**
+ * Creates VespaSpooler objects using the given xml Element.
+ */
+ private void createSpoolers(AbstractConfigProducer pcp, Element element, Clients clients) {
+ String jvmArgs = null;
+ if (element.hasAttribute(VespaDomBuilder.JVMARGS_ATTRIB_NAME)) jvmArgs=element.getAttribute(VespaDomBuilder.JVMARGS_ATTRIB_NAME);
+ SimpleConfigProducer spoolerCfg = new VespaDomBuilder.DomSimpleConfigProducerBuilder(element.getNodeName()).
+ build(pcp, element);
+ Element spoolersFeederOptions = findFeederOptions(element);
+ createSpoolMasters(spoolerCfg, element);
+ for (Element e : XML.getChildren(element, "spooler")) {
+ String configId = e.getAttribute("id").trim();
+ FeederConfig.Builder feederConfig = getFeederConfig(spoolersFeederOptions, e);
+ SpoolerConfig.Builder spoolConfig = getSpoolConfig(e);
+ if (configId.length() == 0) {
+ int index = clients.getVespaSpoolers().size();
+ VespaSpoolerService spoolerService = new VespaSpoolerServiceBuilder(index, new VespaSpooler(feederConfig, spoolConfig)).
+ build(spoolerCfg, e);
+ if ("".equals(spoolerService.getJvmArgs()) && jvmArgs!=null) spoolerService.setJvmArgs(jvmArgs);
+ spoolerService.setProp("index", String.valueOf(index));
+ clients.getVespaSpoolers().add(spoolerService);
+ } else {
+ new VespaSpoolerProducerBuilder(configId, new VespaSpooler(feederConfig, spoolConfig)).
+ build(spoolerCfg, e);
+ }
+ }
+ }
+
+ private void createSpoolMasters(SimpleConfigProducer producer,
+ Element element) {
+ int i=0;
+ for (Element e : XML.getChildren(element, "spoolmaster")) {
+ VespaSpoolMaster master = new VespaSpoolMasterBuilder(i).build(producer, e);
+ i++;
+ }
+ }
+
+ private SpoolerConfig.Builder getSpoolConfig(Element conf) {
+ SpoolerConfig.Builder builder = new SpoolerConfig.Builder();
+ if (conf.getAttributes().getNamedItem("directory") != null) {
+ builder.directory(Defaults.getDefaults().underVespaHome(conf.getAttributes().getNamedItem("directory").getNodeValue()));
+ }
+ if (conf.getAttributes().getNamedItem("keepsuccess") != null) {
+ builder.keepsuccess(getBooleanFromAttribute(conf, "keepsuccess"));
+ }
+ if (conf.getAttributes().getNamedItem("maxfailuresize") != null) {
+ builder.maxfailuresize(getIntegerFromAttribute(conf, "maxfailuresize"));
+ }
+ if (conf.getAttributes().getNamedItem("maxfatalfailuresize") != null) {
+ builder.maxfatalfailuresize(getIntegerFromAttribute(conf, "maxfatalfailuresize"));
+ }
+ if (conf.getAttributes().getNamedItem("threads") != null) {
+ builder.threads(getIntegerFromAttribute(conf, "threads"));
+ }
+ if (conf.getAttributes().getNamedItem("maxretries") != null) {
+ builder.maxretries(getIntegerFromAttribute(conf, "maxretries"));
+ }
+
+ NodeList children = conf.getElementsByTagName("parsers");
+ if (children.getLength() == 1) {
+ children = ((Element)children.item(0)).getElementsByTagName("parser");
+ }
+
+ for (int i=0; i < children.getLength(); i++) {
+ Element e = (Element)children.item(i);
+
+ String type = e.getAttributes().getNamedItem("type").getNodeValue();
+ NodeList params = e.getElementsByTagName("parameter");
+
+ SpoolerConfig.Parsers.Builder parserBuilder = new SpoolerConfig.Parsers.Builder();
+ parserBuilder.classname(type);
+ if (params.getLength() > 0) {
+ List<SpoolerConfig.Parsers.Parameters.Builder> parametersBuilders = new ArrayList<>();
+ for (int j = 0; j < params.getLength(); j++) {
+ SpoolerConfig.Parsers.Parameters.Builder parametersBuilder = new SpoolerConfig.Parsers.Parameters.Builder();
+ Element p = (Element) params.item(j);
+ parametersBuilder.key(getStringFromAttribute(p, "key"));
+ parametersBuilder.value(getStringFromAttribute(p, "value"));
+ parametersBuilders.add(parametersBuilder);
+ }
+ parserBuilder.parameters(parametersBuilders);
+ }
+
+ builder.parsers.add(parserBuilder);
+ }
+ return builder;
+ }
+
+ Boolean getBooleanFromAttribute(Element e, String attributeName) {
+ return Boolean.parseBoolean(e.getAttributes().getNamedItem(attributeName).getNodeValue());
+ }
+
+ Integer getIntegerFromAttribute(Element e, String attributeName) {
+ return Integer.parseInt(e.getAttributes().getNamedItem(attributeName).getNodeValue());
+ }
+
+ String getStringFromAttribute(Element e, String attributeName) {
+ return e.getAttributes().getNamedItem(attributeName).getNodeValue();
+ }
+
+ private FeederConfig.Builder getFeederConfig(Element gatewaysFeederOptions, Element e) {
+ FeederOptionsParser foParser = new FeederOptionsParser();
+ if (gatewaysFeederOptions!=null) {
+ foParser.parseFeederOptions(gatewaysFeederOptions).getFeederConfig();
+ }
+ foParser.parseFeederOptions(e);
+ return foParser.getFeederConfig();
+ }
+
+ /**
+ * Finds the feederoptions subelement in the given xml Element.
+ *
+ * @param element The xml Element
+ * @return The feederoptions xml Element
+ */
+ private Element findFeederOptions(Element element) {
+ for (Element child : XML.getChildren(element)) {
+ if (child.getNodeName().equals("feederoptions")) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private static class VespaSpoolerServiceBuilder extends DomConfigProducerBuilder<VespaSpoolerService> {
+ private int index;
+ private VespaSpooler spoolerConfig;
+
+ public VespaSpoolerServiceBuilder(int index, VespaSpooler spoolerConfig) {
+ this.index = index;
+ this.spoolerConfig = spoolerConfig;
+ }
+
+ @Override
+ protected VespaSpoolerService doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new VespaSpoolerService(parent, index, spoolerConfig);
+ }
+ }
+
+ private static class VespaSpoolerProducerBuilder extends DomConfigProducerBuilder<VespaSpoolerProducer> {
+ private String name=null;
+ private VespaSpooler spooler;
+
+ public VespaSpoolerProducerBuilder(String name, VespaSpooler spooler) {
+ this.name = name;
+ this.spooler = spooler;
+ }
+
+ @Override
+ protected VespaSpoolerProducer doBuild(AbstractConfigProducer parent,
+ Element producerSpec) {
+ return new VespaSpoolerProducer(parent, name, spooler);
+ }
+ }
+
+ private static class VespaSpoolMasterBuilder extends DomConfigProducerBuilder<VespaSpoolMaster> {
+ int index;
+
+ public VespaSpoolMasterBuilder(int index) {
+ super();
+ this.index = index;
+ }
+
+ @Override
+ protected VespaSpoolMaster doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new VespaSpoolMaster(parent, index);
+ }
+ }
+
+ public static class ContainerHttpGatewayClusterBuilder extends DomConfigProducerBuilder<ContainerCluster> {
+ @Override
+ protected ContainerCluster doBuild(AbstractConfigProducer parent,
+ Element spec) {
+
+ ContainerCluster cluster = new ContainerCluster(parent, "gateway", "gateway");
+
+ SearchChains searchChains = new SearchChains(cluster, "searchchain");
+ Set<ComponentSpecification> inherited = new TreeSet<>();
+ //inherited.add(new ComponentSpecification("vespa", null, null));
+ {
+ SearchChain mySearchChain = new SearchChain(new ChainSpecification(new ComponentId("vespaget"),
+ new ChainSpecification.Inheritance(inherited, null), new ArrayList<>(), new TreeSet<>()));
+ Searcher getComponent = newVespaClientSearcher("com.yahoo.storage.searcher.GetSearcher");
+ mySearchChain.addInnerComponent(getComponent);
+ searchChains.add(mySearchChain);
+ }
+ {
+ SearchChain mySearchChain = new SearchChain(new ChainSpecification(new ComponentId("vespavisit"),
+ new ChainSpecification.Inheritance(inherited, null), new ArrayList<>(), new TreeSet<>()));
+ Searcher getComponent = newVespaClientSearcher("com.yahoo.storage.searcher.VisitSearcher");
+ mySearchChain.addInnerComponent(getComponent);
+ searchChains.add(mySearchChain);
+ }
+
+ ContainerSearch containerSearch = new ContainerSearch(cluster, searchChains, new ContainerSearch.Options());
+ cluster.setSearch(containerSearch);
+
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandler", "http://*/feed"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerRemove", "http://*/remove"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation", "http://*/removelocation"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerGet", "http://*/get"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerVisit", "http://*/visit"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerCompatibility", "http://*/document"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerStatus", "http://*/feedstatus"));
+ final ProcessingHandler<SearchChains> searchHandler = new ProcessingHandler<>(
+ cluster.getSearch().getChains(), "com.yahoo.search.handler.SearchHandler");
+ searchHandler.addServerBindings("http://*/search/*");
+ cluster.addComponent(searchHandler);
+
+ ContainerModelBuilder.addDefaultHandler_legacyBuilder(cluster);
+
+ //BEGIN HACK for docproc chains:
+ DocprocChains docprocChains = getDocprocChains(cluster, spec);
+ if (docprocChains != null) {
+ ContainerDocproc containerDocproc = new ContainerDocproc(cluster, docprocChains);
+ cluster.setDocproc(containerDocproc);
+ }
+ //END HACK
+
+ return cluster;
+ }
+
+ private Handler newVespaClientHandler(String componentId, String binding) {
+ Handler<AbstractConfigProducer<?>> handler = new Handler<>(new ComponentModel(
+ BundleInstantiationSpecification.getFromStrings(componentId, null, vespaClientBundleSpecification), ""));
+ handler.addServerBindings(binding);
+ handler.addServerBindings(binding + '/');
+ return handler;
+ }
+
+ private Searcher newVespaClientSearcher(String componentSpec) {
+ return new Searcher<>(new ChainedComponentModel(
+ BundleInstantiationSpecification.getFromStrings(componentSpec, null, vespaClientBundleSpecification),
+ new Dependencies(null, null, null)));
+ }
+
+ //BEGIN HACK for docproc chains:
+ private DocprocChains getDocprocChains(AbstractConfigProducer qrs, Element gateways) {
+ Element clients = (Element) gateways.getParentNode();
+ Element services = (Element) clients.getParentNode();
+ if (services == null) {
+ return null;
+ }
+
+ Element docproc = XML.getChild(services, "docproc");
+ if (docproc == null) {
+ return null;
+ }
+
+ String version = docproc.getAttribute("version");
+ if (version.startsWith("1.")) {
+ return null;
+ } else if (version.startsWith("2.")) {
+ return null;
+ } else if (version.startsWith("3.")) {
+ return getDocprocChainsV3(qrs, docproc);
+ } else {
+ throw new IllegalArgumentException("Docproc version " + version + " unknown.");
+ }
+ }
+
+ private DocprocChains getDocprocChainsV3(AbstractConfigProducer qrs, Element docproc) {
+ Element docprocChainsElem = XML.getChild(docproc, "docprocchains");
+ if (docprocChainsElem == null) {
+ return null;
+ }
+ return new DomDocprocChainsBuilder(null, true).build(qrs, docprocChainsElem);
+ }
+ //END HACK
+ }
+
+ public static class ContainerHttpGatewayBuilder extends DomConfigProducerBuilder<ContainerHttpGateway> {
+ int index;
+ ContainerCluster cluster;
+
+ public ContainerHttpGatewayBuilder(ContainerCluster cluster, int index) {
+ this.index = index;
+ this.cluster = cluster;
+ }
+
+ @Override
+ protected ContainerHttpGateway doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ // TODO: remove port handling
+ int port = 19020;
+ if (spec != null && spec.hasAttribute("baseport")) {
+ port = Integer.parseInt(spec.getAttribute("baseport"));
+ }
+ ContainerHttpGateway httpGateway = new ContainerHttpGateway(cluster, "" + index, port);
+ List<Container> containers = new ArrayList<>();
+ containers.add(httpGateway);
+
+ cluster.addContainers(containers);
+ return httpGateway;
+ }
+ }
+
+ /**
+ * This class parses the feederoptions xml tag and produces Vespa config output.
+ *
+ * @author <a href="mailto:gunnarga@yahoo-inc.com">Gunnar Gauslaa Bergem</a>
+ */
+ private class FeederOptionsParser implements Serializable {
+ private static final long serialVersionUID = 1L;
+ // All member variables are objects so that we can switch on null values.
+ private Boolean abortondocumenterror = null;
+ private String route = null;
+ private Integer maxpendingdocs = null;
+ private Integer maxpendingbytes = null;
+ private Boolean retryenabled = null;
+ private Double retrydelay = null;
+ private Double timeout = null;
+ private Integer tracelevel = null;
+ private Integer mbusport = null;
+ private String docprocChain = null;
+
+ /**
+ * Constructs an empty feeder options object with all members set to null.
+ */
+ public FeederOptionsParser() {
+ // empty
+ }
+
+ /**
+ * Parses the content of the given XML element as feeder options.
+ *
+ * @param conf The XML element to parse.
+ */
+ public FeederOptionsParser parseFeederOptions(Element conf) {
+ for (Node node : XML.getChildren(conf)) {
+ String nodename = node.getNodeName();
+ Node firstchild = node.getFirstChild();
+ String childval = (firstchild != null) ? firstchild.getNodeValue() : null;
+
+ switch (nodename) {
+ case "abortondocumenterror":
+ abortondocumenterror = Boolean.valueOf(childval);
+ break;
+ case "maxpendingdocs":
+ maxpendingdocs = new Integer(childval);
+ break;
+ case "maxpendingbytes":
+ maxpendingbytes = new Integer(childval);
+ break;
+ case "retryenabled":
+ retryenabled = Boolean.valueOf(childval);
+ break;
+ case "retrydelay":
+ retrydelay = new Double(childval);
+ break;
+ case "timeout":
+ timeout = new Double(childval);
+ break;
+ case "route":
+ route = childval;
+ break;
+ case "tracelevel":
+ tracelevel = new Integer(childval);
+ break;
+ case "mbusport":
+ mbusport = new Integer(childval);
+ break;
+ case "docprocchain":
+ docprocChain = childval;
+ break;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Returns a feeder options config string of the content of this.
+ *
+ * @return A config string.
+ */
+ public FeederConfig.Builder getFeederConfig() {
+ FeederConfig.Builder builder = new FeederConfig.Builder();
+ if (abortondocumenterror != null) {
+ builder.abortondocumenterror(abortondocumenterror);
+ }
+ if (route != null && route.length() > 0) {
+ builder.route(route);
+ }
+ if (maxpendingdocs != null) {
+ builder.maxpendingdocs(maxpendingdocs);
+ }
+ if (maxpendingbytes != null) {
+ builder.maxpendingbytes(maxpendingbytes);
+ }
+ if (retryenabled != null) {
+ builder.retryenabled(retryenabled);
+ }
+ if (retrydelay != null) {
+ builder.retrydelay(retrydelay);
+ }
+ if (timeout != null) {
+ builder.timeout(timeout);
+ }
+ if (tracelevel != null) {
+ builder.tracelevel(tracelevel);
+ }
+ if (mbusport != null) {
+ builder.mbusport(mbusport);
+ }
+ if (docprocChain != null && docprocChain.length() > 0) {
+ builder.docprocchain(docprocChain);
+ }
+ return builder;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilder.java
new file mode 100644
index 00000000000..87a88fae176
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilder.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.ConfigModelInstanceFactory;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import org.w3c.dom.Element;
+
+/**
+ * A model builder that can be used to deal with toplevel config overrides and create another
+ * producer in between. This should not be used by new model plugins.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public abstract class LegacyConfigModelBuilder<MODEL extends ConfigModel> extends ConfigModelBuilder<MODEL> {
+
+ public LegacyConfigModelBuilder(Class<MODEL> configModelClass) {
+ super(configModelClass);
+ }
+
+ @Override
+ public MODEL build(ConfigModelInstanceFactory<MODEL> factory, Element spec, ConfigModelContext context) {
+ VespaDomBuilder.DomSimpleConfigProducerBuilder builder = new VespaDomBuilder.DomSimpleConfigProducerBuilder(context.getProducerId());
+ AbstractConfigProducer producer = builder.build(context.getParentProducer(), spec);
+ return super.build(factory, spec, context.modifyParent(producer));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java
new file mode 100644
index 00000000000..a60e0dd01e4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ModelElement.java
@@ -0,0 +1,211 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.utils.Duration;
+
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * A w3c Element wrapper whith a better API.
+ *
+ * Author unknown.
+ */
+public class ModelElement {
+
+ private final Element xml;
+
+ public ModelElement(Element xml) {
+ this.xml = xml;
+ if (xml == null) {
+ throw new NullPointerException("Can not create ModelElement with null element");
+ }
+ if (xml.getNodeName() == null) {
+ throw new NullPointerException("Can not create ModelElement with unnamed element");
+ }
+ }
+
+ public Element getXml() {
+ return xml;
+ }
+
+ /**
+ * If not found, return null.
+ */
+ public ModelElement getChild(String name) {
+ Element e = XML.getChild(xml, name);
+
+ if (e != null) {
+ return new ModelElement(e);
+ }
+
+ return null;
+ }
+
+ public ModelElement getChildByPath(String path) {
+ StringTokenizer tokenizer = new StringTokenizer(path, ".");
+ ModelElement curElem = this;
+ while (tokenizer.hasMoreTokens() && curElem != null) {
+ String pathElem = tokenizer.nextToken();
+ ModelElement child = curElem.getChild(pathElem);
+ if (!tokenizer.hasMoreTokens()) {
+ if (child != null) {
+ return child;
+ }
+ }
+ curElem = child;
+ }
+ return null;
+ }
+
+ public String childAsString(String path) {
+ StringTokenizer tokenizer = new StringTokenizer(path, ".");
+ ModelElement curElem = this;
+ while (tokenizer.hasMoreTokens() && curElem != null) {
+ String pathElem = tokenizer.nextToken();
+ ModelElement child = curElem.getChild(pathElem);
+ if (!tokenizer.hasMoreTokens()) {
+ String attr = curElem.getStringAttribute(pathElem);
+ if (attr != null) {
+ return attr;
+ } else if (child != null) {
+ return child.asString();
+ }
+ }
+ curElem = child;
+ }
+ return null;
+ }
+
+ public String asString() {
+ return xml.getFirstChild().getTextContent();
+ }
+
+ public double asDouble() {
+ return Double.parseDouble(asString());
+ }
+
+ public long asLong() {
+ return (long) BinaryUnit.valueOf(asString());
+ }
+
+ public Duration asDuration() {
+ return new Duration(asString());
+ }
+
+ public Long childAsLong(String path) {
+ String child = childAsString(path);
+ if (child == null) {
+ return null;
+ }
+ return Long.parseLong(child.trim());
+ }
+
+ public Integer childAsInteger(String path) {
+ String child = childAsString(path);
+ if (child == null) {
+ return null;
+ }
+ return Integer.parseInt(child.trim());
+ }
+
+ public Double childAsDouble(String path) {
+ String child = childAsString(path);
+ if (child == null) {
+ return null;
+ }
+ return Double.parseDouble(child.trim());
+ }
+
+ public Boolean childAsBoolean(String path) {
+ String child = childAsString(path);
+ if (child == null) {
+ return null;
+ }
+ return Boolean.parseBoolean(child.trim());
+ }
+
+ public Duration childAsDuration(String path) {
+ String child = childAsString(path);
+ if (child == null) {
+ return null;
+ }
+ return new Duration(child);
+ }
+
+ /** Returns the given attribute or throws IllegalArgumentException if not present */
+ public int requiredIntegerAttribute(String name) {
+ if (getStringAttribute(name) == null)
+ throw new IllegalArgumentException("Required attribute '" + name + "' is missing");
+ return getIntegerAttribute(name, null);
+ }
+
+ /** Returns the value of this attribute or null if not present */
+ public Integer getIntegerAttribute(String name) {
+ return getIntegerAttribute(name, null);
+ }
+
+ public Integer getIntegerAttribute(String name, Integer defaultValue) {
+ String value = getStringAttribute(name);
+ if (value == null) {
+ return defaultValue;
+ }
+ return (int) BinaryUnit.valueOf(value);
+ }
+
+ public boolean getBooleanAttribute(String name) {
+ return getBooleanAttribute(name, false);
+ }
+
+ public boolean getBooleanAttribute(String name, boolean defaultValue) {
+ String value = getStringAttribute(name);
+ if (value == null) {
+ return defaultValue;
+ }
+ return Boolean.parseBoolean(value);
+ }
+
+ public Long getLongAttribute(String name) {
+ String value = getStringAttribute(name);
+ if (value == null) {
+ return null;
+ }
+ return (long) BinaryUnit.valueOf(value);
+ }
+
+ public Double getDoubleAttribute(String name) {
+ String value = getStringAttribute(name);
+ if (value == null) {
+ return null;
+ }
+ return Double.parseDouble(value);
+ }
+
+ public String getStringAttribute(String name) {
+ if (!xml.hasAttribute(name)) {
+ return null;
+ }
+
+ return xml.getAttribute(name);
+ }
+
+ public List<ModelElement> subElements(String name) {
+ List<Element> elements = XML.getChildren(xml, name);
+
+ List<ModelElement> helpers = new ArrayList<>();
+ for (Element e : elements) {
+ helpers.add(new ModelElement(e));
+ }
+
+ return helpers;
+ }
+
+ @Override
+ public String toString() {
+ return xml.getNodeName();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java
new file mode 100644
index 00000000000..a27739b42ef
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/NodesSpecification.java
@@ -0,0 +1,105 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.provision.Capacity;
+import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.HostSystem;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * A common utility class to represent a requirement for some nodes during model building.
+ * Such a requirement is commonly specified as a <code>nodes</code> element.
+ *
+ * @author bratseth
+ */
+ // TODO: Use this for all nodes tags and unify with NodesUtil
+public class NodesSpecification {
+
+ private final boolean dedicated;
+
+ private final int count;
+
+ private final int groups;
+
+ private final Optional<String> flavor;
+
+ private final Optional<String> dockerImage;
+
+ private NodesSpecification(boolean dedicated, int count, int groups, Optional<String> flavor, Optional<String> dockerImage) {
+ this.dedicated = dedicated;
+ this.count = count;
+ this.groups = groups;
+ this.flavor = flavor;
+ this.dockerImage = dockerImage;
+ }
+
+ private NodesSpecification(boolean dedicated, ModelElement nodesElement) {
+ this(dedicated,
+ nodesElement.requiredIntegerAttribute("count"),
+ nodesElement.getIntegerAttribute("groups", 1),
+ Optional.ofNullable(nodesElement.getStringAttribute("flavor")),
+ Optional.ofNullable(nodesElement.getStringAttribute("docker-image")));
+ }
+
+ /**
+ * Returns a requirement for dedicated nodes taken from the given <code>nodes</code> element
+ */
+ public static NodesSpecification from(ModelElement nodesElement) {
+ return new NodesSpecification(true, nodesElement);
+ }
+
+ /**
+ * Returns a requirement for dedicated nodes taken from the <code>nodes</code> element
+ * contained in the given parent element, or empty if the parent element is null, or the nodes elements
+ * is not present.
+ */
+ public static Optional<NodesSpecification> fromParent(ModelElement parentElement) {
+ if (parentElement == null) return Optional.empty();
+ ModelElement nodesElement = parentElement.getChild("nodes");
+ if (nodesElement == null) return Optional.empty();
+ return Optional.of(from(nodesElement));
+ }
+
+ /**
+ * Returns a requirement for undedicated or dedicated nodes taken from the <code>nodes</code> element
+ * contained in the given parent element, or empty if the parent element is null, or the nodes elements
+ * is not present.
+ */
+ public static Optional<NodesSpecification> optionalDedicatedFromParent(ModelElement parentElement) {
+ if (parentElement == null) return Optional.empty();
+ ModelElement nodesElement = parentElement.getChild("nodes");
+ if (nodesElement == null) return Optional.empty();
+ return Optional.of(new NodesSpecification(nodesElement.getBooleanAttribute("dedicated", false), nodesElement));
+ }
+
+ /** Returns a requirement from <code>count</code> nondedicated nodes in one group */
+ public static NodesSpecification nonDedicated(int count) {
+ return new NodesSpecification(false, count, 1, Optional.empty(), Optional.empty());
+ }
+
+ /**
+ * Returns whether this requires dedicated nodes.
+ * Otherwise the model encountering this request should reuse nodes requested for other purposes whenever possible.
+ */
+ public boolean isDedicated() { return dedicated; }
+
+ /** Returns the number of nodes required */
+ public int count() { return count; }
+
+ /** Returns the number of host groups this specifies. Default is 1 */
+ public int groups() { return groups; }
+
+ public Map<HostResource, ClusterMembership> provision(HostSystem hostSystem, ClusterSpec.Type clusterType, ClusterSpec.Id clusterId, Optional<ClusterSpec.Group> clusterGroup, DeployLogger logger) {
+ if (clusterGroup.isPresent() && groups > 1)
+ throw new IllegalArgumentException("Cannot both specify a group and request multiple groups");
+ ClusterSpec cluster = ClusterSpec.from(clusterType, clusterId, clusterGroup, dockerImage);
+ return hostSystem.allocateHosts(cluster, Capacity.fromNodeCount(count, flavor), groups, logger);
+ }
+
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ServletBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ServletBuilder.java
new file mode 100644
index 00000000000..147f6517754
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/ServletBuilder.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.container.component.Servlet;
+import com.yahoo.vespa.model.container.component.ServletProvider;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+import com.yahoo.vespa.model.container.xml.BundleInstantiationSpecificationBuilder;
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author stiankri
+ * @since 5.32
+ */
+public class ServletBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Servlet> {
+ @Override
+ protected ServletProvider doBuild(AbstractConfigProducer ancestor, Element servletElement) {
+ SimpleComponent servlet = createServletComponent(servletElement);
+ ServletProvider servletProvider = createServletProvider(servletElement, servlet);
+
+ return servletProvider;
+ }
+
+ private SimpleComponent createServletComponent(Element servletElement) {
+ ComponentModel componentModel = new ComponentModel(BundleInstantiationSpecificationBuilder.build(servletElement, false));
+ return new SimpleComponent(componentModel);
+ }
+
+ private ServletProvider createServletProvider(Element servletElement, SimpleComponent servlet) {
+ Map<String, String> servletConfig = getServletConfig(servletElement);
+ return new ServletProvider(servlet, getPath(servletElement), servletConfig);
+ }
+
+ private String getPath(Element servletElement) {
+ Element pathElement = XML.getChild(servletElement, "path");
+ return XML.getValue(pathElement);
+ }
+
+ private Map<String, String> getServletConfig(Element servletElement) {
+ Map<String, String> servletConfig = new HashMap<>();
+
+ Element servletConfigElement = XML.getChild(servletElement, "servlet-config");
+ XML.getChildren(servletConfigElement).forEach( parameter ->
+ servletConfig.put(parameter.getTagName(), XML.getValue(parameter))
+ );
+
+ return servletConfig;
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilder.java
new file mode 100644
index 00000000000..08c3db81091
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilder.java
@@ -0,0 +1,301 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.*;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.UserConfigRepo;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.*;
+import com.yahoo.vespa.model.builder.UserConfigBuilder;
+import com.yahoo.vespa.model.builder.VespaModelBuilder;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.generic.builder.DomServiceClusterBuilder;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Builds Vespa model components using the w3c dom api
+ *
+ * @author vegardh
+ */
+public class VespaDomBuilder extends VespaModelBuilder {
+
+ public static final String JVMARGS_ATTRIB_NAME="jvmargs";
+ public static final String PRELOAD_ATTRIB_NAME="preload";
+ public static final String MMAP_NOCORE_LIMIT="mmap-nocore-limit";
+ private static final String CPU_SOCKET_ATTRIB_NAME = "cpu-socket";
+ public static final String CPU_SOCKET_AFFINITY_ATTRIB_NAME = "cpu-socket-affinity";
+
+ public static final Logger log = Logger.getLogger(VespaDomBuilder.class.getPackage().toString());
+
+ /**
+ * Get all aliases for one host from a list of 'alias' xml nodes.
+ *
+ * @param hostAliases List of xml nodes, each representing one hostalias
+ * @return a list of alias strings.
+ */
+ // TODO Move and change scope
+ public static List<String> getHostAliases(NodeList hostAliases) {
+ List<String> aliases = new LinkedList<>();
+ for (int i=0; i < hostAliases.getLength(); i++) {
+ Node n = hostAliases.item(i);
+ if (! (n instanceof Element)) {
+ continue;
+ }
+ Element e = (Element)n;
+ if (! e.getNodeName().equals("alias")) {
+ throw new RuntimeException("Unexpected tag: '" + e.getNodeName() + "' at node " +
+ XML.getNodePath(e, " > ") + ", expected 'alias'.");
+ }
+ String alias = e.getFirstChild().getNodeValue();
+ if ((alias == null) || (alias.equals(""))) {
+ throw new RuntimeException("Missing value for the alias tag at node " +
+ XML.getNodePath(e, " > ") + "'.");
+ }
+ aliases.add(alias);
+ }
+ return aliases;
+ }
+
+
+ @Override
+ public ApplicationConfigProducerRoot getRoot(String name, DeployState deployState, AbstractConfigProducer parent) {
+ try {
+ return new DomRootBuilder(name, deployState).
+ build(parent, XmlHelper.getDocument(deployState.getApplicationPackage().getServices()).getDocumentElement());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * @param spec The element containing the xml specification for this Service.
+ * @return the user's desired port, which is retrieved from the xml spec.
+ */
+ public static int getXmlWantedPort(Element spec) {
+ return getXmlIntegerAttribute(spec, "baseport");
+ }
+
+ /**
+ * Base class for builders of producers using DOM. The purpose is to always
+ * include hostalias, baseport and user config overrides generically.
+ *
+ * @param <T> an {@link com.yahoo.config.model.producer.AbstractConfigProducer}
+ * @author vegardh
+ */
+ public static abstract class DomConfigProducerBuilder<T extends AbstractConfigProducer> {
+
+ // TODO: find good way to provide access to app package
+ public final T build(AbstractConfigProducer ancestor, Element producerSpec) {
+ T t = doBuild(ancestor, producerSpec);
+
+ if (t instanceof AbstractService) {
+ initializeService((AbstractService)t, ancestor, producerSpec);
+ } else {
+ initializeProducer(t, ancestor, producerSpec);
+ }
+
+ return t;
+ }
+
+ protected abstract T doBuild(AbstractConfigProducer ancestor, Element producerSpec);
+
+ private void initializeProducer(AbstractConfigProducer child,
+ AbstractConfigProducer ancestor,
+ Element producerSpec) {
+ UserConfigRepo userConfigs = UserConfigBuilder.build(producerSpec, ancestor.getRoot().getDeployState(), ancestor.getRoot().deployLogger());
+ // TODO: must be made to work:
+ //userConfigs.applyWarnings(child);
+ log.log(LogLevel.DEBUG, "Adding user configs " + userConfigs + " for " + producerSpec);
+ child.mergeUserConfigs(userConfigs);
+ }
+
+ private void initializeService(AbstractService t,
+ AbstractConfigProducer ancestor,
+ Element producerSpec) {
+ initializeProducer(t, ancestor, producerSpec);
+ if (producerSpec != null) {
+ if (producerSpec.hasAttribute(JVMARGS_ATTRIB_NAME)) {
+ t.appendJvmArgs(producerSpec.getAttribute(JVMARGS_ATTRIB_NAME));
+ }
+ if (producerSpec.hasAttribute(PRELOAD_ATTRIB_NAME)) {
+ t.setPreLoad(producerSpec.getAttribute(PRELOAD_ATTRIB_NAME));
+ }
+ if (producerSpec.hasAttribute(MMAP_NOCORE_LIMIT)) {
+ t.setMMapNoCoreLimit(Long.parseLong(producerSpec.getAttribute(MMAP_NOCORE_LIMIT)));
+ }
+ if (producerSpec.hasAttribute(CPU_SOCKET_ATTRIB_NAME)) {
+ t.setAffinity(new Affinity.Builder().cpuSocket(Integer.parseInt(producerSpec.getAttribute(CPU_SOCKET_ATTRIB_NAME))).build());
+ }
+ int port = getXmlWantedPort(producerSpec);
+ if (port > 0) {
+ t.setBasePort(port);
+ }
+ allocateHost(t, ancestor.getHostSystem(), producerSpec);
+ }
+ // This depends on which constructor in AbstractService is used, but the best way
+ // is to let this method do initialize.
+ if (!t.isInitialized()) {
+ t.initService();
+ }
+ }
+
+ /**
+ * Allocates a host to the service using host file or create service spec for provisioner to use later
+ * Pre-condition: producerSpec is non-null
+ * @param service the service to allocate a host for
+ * @param hostSystem a {@link HostSystem}
+ * @param producerSpec xml element for the service
+ */
+ private void allocateHost(final AbstractService service, HostSystem hostSystem, Element producerSpec) {
+ // TODO store service on something else than HostSystem, to not make that overloaded
+ service.setHostResource(hostSystem.getHost(producerSpec.getAttribute("hostalias")));
+ }
+ }
+
+ /**
+ * The SimpleConfigProducer is the producer for elements such as qrservers, topleveldispatchers, gateways.
+ * Must support overrides for that too, hence this builder
+ *
+ * @author vegardh
+ */
+ public static class DomSimpleConfigProducerBuilder extends DomConfigProducerBuilder<SimpleConfigProducer> {
+ private String configId = null;
+
+ public DomSimpleConfigProducerBuilder(String configId) {
+ this.configId = configId;
+ }
+
+ @Override
+ protected SimpleConfigProducer doBuild(AbstractConfigProducer parent,
+ Element producerSpec) {
+ return new SimpleConfigProducer(parent, configId);
+ }
+ }
+
+ public static class DomRootBuilder extends VespaDomBuilder.DomConfigProducerBuilder<ApplicationConfigProducerRoot> {
+ private final String name;
+ private final DeployState deployState;
+
+ /**
+ * @param name The name of the Vespa to create. Usually 'root' when there is only one.
+ */
+ public DomRootBuilder(String name, DeployState deployState) {
+ this.name = name;
+ this.deployState = deployState;
+ }
+
+ @Override
+ protected ApplicationConfigProducerRoot doBuild(AbstractConfigProducer parent, Element producerSpec) {
+ ApplicationConfigProducerRoot root = new ApplicationConfigProducerRoot(parent, name,
+ deployState.getDocumentModel(), deployState.getProperties().vespaVersion(), deployState.getProperties().applicationId());
+ root.setHostSystem(new HostSystem(root, "hosts", deployState.getProvisioner()));
+ new Client(root);
+ return root;
+ }
+ }
+
+ /**
+ * Gets the index from a service's spec
+ *
+ * @param spec The element containing the xml specification for this Service.
+ * @return the index of the service, which is retrieved from the xml spec.
+ */
+ static int getIndex(Element spec) {
+ return getXmlIntegerAttribute(spec, "index");
+ }
+
+ /**
+ * Gets an integer attribute value from a service's spec
+ *
+ * @param spec XML element
+ * @param attributeName nam of attribute to get value from
+ * @return value of attribute, or 0 if it does not exist or is empty
+ */
+ static int getXmlIntegerAttribute(Element spec, String attributeName) {
+ String value = (spec == null) ? null : spec.getAttribute(attributeName);
+ if (value == null || value.equals("")) {
+ return 0;
+ } else {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException
+ ("Illegal format for attribute '" + attributeName + "' at node " +
+ XML.getNodePath(spec, " > ") + ", must be an integer", e);
+ }
+ }
+ }
+
+ /**
+ * Processing that requires access across different plugins
+ *
+ * @param root root config producer
+ * @param configModelRepo a {@link ConfigModelRepo}
+ */
+ public void postProc(AbstractConfigProducer root, ConfigModelRepo configModelRepo) {
+ createTlds(configModelRepo);
+ setContentSearchClusterIndexes(configModelRepo);
+ createDocprocMBusServersAndClients(configModelRepo);
+ }
+
+ private void createDocprocMBusServersAndClients(ConfigModelRepo pc) {
+ for (ContainerCluster cluster: ContainerModel.containerClusters(pc)) {
+ addServerAndClientsForChains(cluster.getDocproc());
+ }
+ }
+
+ private void addServerAndClientsForChains(ContainerDocproc docproc) {
+ if (docproc != null)
+ docproc.getChains().addServersAndClientsForChains();
+ }
+
+ private void createTlds(ConfigModelRepo pc) {
+ for (ConfigModel p : pc.asMap().values()) {
+ if (p instanceof Content) {
+ ((Content)p).createTlds(pc);
+ }
+ }
+ }
+
+ /**
+ * For some reason, search clusters need to be enumerated.
+ * @param configModelRepo a {@link ConfigModelRepo}
+ */
+ private void setContentSearchClusterIndexes(ConfigModelRepo configModelRepo) {
+ int index = 0;
+ for (AbstractSearchCluster sc : Content.getSearchClusters(configModelRepo)) {
+ sc.setClusterIndex(index++);
+ }
+ }
+
+ @Override
+ public List<ServiceCluster> getClusters(ApplicationPackage pkg,
+ AbstractConfigProducer parent) {
+ List<ServiceCluster> clusters = new ArrayList<>();
+ Document services = XmlHelper.getDocument(pkg.getServices());
+ for (Element clusterSpec : XML.getChildren(services.getDocumentElement(), "cluster")) {
+ DomServiceClusterBuilder clusterBuilder = new DomServiceClusterBuilder(clusterSpec.getAttribute("name"));
+ clusters.add(clusterBuilder.build(parent.getRoot(), clusterSpec));
+ }
+ return clusters;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainSpecificationBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainSpecificationBuilder.java
new file mode 100644
index 00000000000..9dac8a23587
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainSpecificationBuilder.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.text.XML;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+
+/**
+ * Creates a partial ChainSpecification without inner components
+ * @author tonytv
+ */
+public class ChainSpecificationBuilder {
+ private final ComponentId componentId;
+ private final ChainSpecification.Inheritance inheritance;
+ private final Collection<Phase> phases;
+
+ public ChainSpecificationBuilder(Element chainElement) {
+ componentId = readComponentId(chainElement);
+ inheritance = readInheritance(chainElement);
+ phases = readPhases(chainElement);
+ }
+
+ private Set<Phase> readPhases(Element parentElement) {
+ Set<Phase> phases = new LinkedHashSet<>();
+
+ for (Element phaseSpec : XML.getChildren(parentElement, "phase")) {
+ String name = XmlHelper.getIdString(phaseSpec);
+ Dependencies dependencies = new DependenciesBuilder(phaseSpec).build();
+ phases.add(new Phase(name, dependencies));
+ }
+ return phases;
+ }
+
+ private ComponentId readComponentId(Element spec) {
+ return XmlHelper.getId(spec);
+ }
+
+ private ChainSpecification.Inheritance readInheritance(Element spec) {
+ return new InheritanceBuilder(spec).build();
+ }
+
+ public ChainSpecification build(Set<ComponentSpecification> outerComponentReferences) {
+ return new ChainSpecification(componentId, inheritance, phases, outerComponentReferences);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainedComponentModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainedComponentModelBuilder.java
new file mode 100644
index 00000000000..7414723c164
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainedComponentModelBuilder.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.vespa.model.container.xml.BundleInstantiationSpecificationBuilder;
+import org.w3c.dom.Element;
+
+/**
+ * Builds a regular ChainedComponentModel from an element.
+ * @author tonytv
+ */
+public class ChainedComponentModelBuilder extends GenericChainedComponentModelBuilder {
+ protected final BundleInstantiationSpecification bundleInstantiationSpec;
+
+ public ChainedComponentModelBuilder(Element spec) {
+ super(spec);
+ bundleInstantiationSpec = BundleInstantiationSpecificationBuilder.build(spec, false);
+ }
+
+ public ChainedComponentModel build() {
+ return new ChainedComponentModel(bundleInstantiationSpec, dependencies);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainsBuilder.java
new file mode 100644
index 00000000000..9c848e3f284
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ChainsBuilder.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class ChainsBuilder<COMPONENT extends ChainedComponent<?>, CHAIN extends Chain<COMPONENT>> {
+
+ protected final List<CHAIN> chains = new ArrayList<>();
+ private final Map<String, Class<? extends DomChainBuilderBase<? extends COMPONENT, ? extends CHAIN>>> chainType2BuilderClass;
+
+ // NOTE: The chain type string (key in chainType2BuilderClass) must match the xml tag name for the chain.
+ public ChainsBuilder(AbstractConfigProducer ancestor, List<Element> chainsElems,
+ Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName,
+ Map<String, Class<? extends DomChainBuilderBase<? extends COMPONENT, ? extends CHAIN>>> chainType2BuilderClass) {
+
+ this.chainType2BuilderClass = chainType2BuilderClass;
+ readChains(ancestor, chainsElems, outerComponentTypeByComponentName);
+ }
+
+ public Collection<CHAIN> getChains() {
+ return Collections.unmodifiableCollection(chains);
+ }
+
+ private void readChains(AbstractConfigProducer ancestor, List<Element> chainsElems,
+ Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+
+ for (Map.Entry<String, Class<? extends DomChainBuilderBase<? extends COMPONENT, ? extends CHAIN>>>
+ chainType : chainType2BuilderClass.entrySet()) {
+ for (Element elemContainingChainElems : chainsElems) {
+ for (Element chainElem : XML.getChildren(elemContainingChainElems, chainType.getKey())) {
+ readChain(ancestor, chainElem, chainType.getValue(), outerSearcherTypeByComponentName);
+ }
+ }
+ }
+ }
+
+ private void readChain(AbstractConfigProducer ancestor, Element chainElem,
+ Class<? extends DomChainBuilderBase<? extends COMPONENT, ? extends CHAIN>> builderClass,
+ Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+
+ DomChainBuilderBase<? extends COMPONENT, ? extends CHAIN> builder =
+ DomBuilderCreator.create(builderClass, outerSearcherTypeByComponentName);
+ chains.add(builder.build(ancestor, chainElem));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ComponentsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ComponentsBuilder.java
new file mode 100644
index 00000000000..b7aef6b9b1f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/ComponentsBuilder.java
@@ -0,0 +1,156 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.docproc.DomDocumentProcessorBuilder;
+import com.yahoo.vespa.model.container.http.xml.FilterBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.processing.DomProcessorBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.search.DomFederationSearcherBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.search.DomSearcherBuilder;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import com.yahoo.vespa.model.container.http.Filter;
+import com.yahoo.vespa.model.container.processing.Processor;
+import com.yahoo.vespa.model.container.docproc.DocumentProcessor;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * Creates component models and component references from xml for a given scope.
+ * @author tonytv
+ */
+public class ComponentsBuilder<T extends ChainedComponent<?>> {
+
+ // NOTE: the 'name' string must match the xml tag name for the component in services.
+ public static class ComponentType<T extends ChainedComponent<?>> {
+ static ArrayList<ComponentType> values = new ArrayList<>();
+ public static final ComponentType<DocumentProcessor> documentprocessor = new ComponentType<>("documentprocessor", DomDocumentProcessorBuilder.class);
+ public static final ComponentType<Searcher<?>> searcher = new ComponentType<>("searcher", DomSearcherBuilder.class);
+ public static final ComponentType<Processor> processor = new ComponentType<>("processor", DomProcessorBuilder.class);
+ public static final ComponentType<Searcher<?>> federation = new ComponentType<>("federation", DomFederationSearcherBuilder.class);
+ public static final ComponentType<Filter> filter = new ComponentType<>("filter", FilterBuilder.class);
+
+
+ final String name;
+
+ private final Class<? extends VespaDomBuilder.DomConfigProducerBuilder<T>> builderClass;
+
+ private ComponentType(String name, Class<? extends VespaDomBuilder.DomConfigProducerBuilder<T>> builderClass) {
+ this.name = name;
+ this.builderClass = builderClass;
+ values.add(this);
+ }
+
+ public VespaDomBuilder.DomConfigProducerBuilder<T> createBuilder() {
+ return DomBuilderCreator.create(builderClass);
+ }
+ }
+
+ private final Set<ComponentSpecification> outerComponentReferences = new LinkedHashSet<>();
+ private final List<T> componentDefinitions = new ArrayList<>();
+ private final Map<String, ComponentType> componentTypesByComponentName = new LinkedHashMap<>();
+
+ /**
+ * @param ancestor The parent config producer
+ * @param componentTypes The allowed component types for 'elementContainingComponentElements' - MUST match &lt;T&gt;
+ * @param elementsContainingComponentElems All elements containing elements with name matching ComponentType.name
+ * @param outerComponentTypeByComponentName Use null if this is the outermost scope, i.e.
+ * every component is a definition, not a reference.
+ */
+ ComponentsBuilder(AbstractConfigProducer ancestor,
+ Collection<ComponentType<T>> componentTypes,
+ List<Element> elementsContainingComponentElems,
+ Map<String, ComponentType> outerComponentTypeByComponentName) {
+
+ readComponents(ancestor, componentTypes, elementsContainingComponentElems, unmodifiable(outerComponentTypeByComponentName));
+ }
+
+ private void readComponents(AbstractConfigProducer ancestor,
+ Collection<ComponentType<T>> componentTypes,
+ List<Element> elementsContainingComponentElems,
+ Map<String, ComponentType> outerComponentTypeByComponentName) {
+
+ for (ComponentType<T> componentType : componentTypes) {
+ for (Element elemContainingComponentElems : elementsContainingComponentElems) {
+ for (Element componentElement : XML.getChildren(elemContainingComponentElems, componentType.name)) {
+ readComponent(ancestor, componentElement, componentType, outerComponentTypeByComponentName);
+ }
+ }
+ }
+ }
+
+ private void readComponent(AbstractConfigProducer ancestor,
+ Element componentElement,
+ ComponentType<T> componentType,
+ Map<String, ComponentType> outerComponentTypeByComponentName) {
+
+ ComponentSpecification componentSpecification = XmlHelper.getIdRef(componentElement);
+
+ if (outerComponentTypeByComponentName.containsKey(componentSpecification.getName())) {
+ readComponentReference(componentElement, componentType, componentSpecification, outerComponentTypeByComponentName);
+ } else {
+ readComponentDefinition(ancestor, componentElement, componentType);
+ }
+ }
+
+ private void readComponentReference(Element componentElement, ComponentType componentType,
+ ComponentSpecification componentSpecification,
+ Map<String, ComponentType> outerComponentTypeByComponentName) {
+
+ String componentName = componentSpecification.getName();
+ ensureTypesMatch(componentType, outerComponentTypeByComponentName.get(componentName), componentName);
+ ensureNotDefinition(componentName, componentElement);
+ outerComponentReferences.add(componentSpecification);
+ }
+
+ private void readComponentDefinition(AbstractConfigProducer ancestor, Element componentElement, ComponentType<T> componentType) {
+ T component = componentType.createBuilder().build(ancestor, componentElement);
+ componentDefinitions.add(component);
+ updateComponentTypes(component.getComponentId(), componentType);
+ }
+
+ private void updateComponentTypes(ComponentId componentId, ComponentType componentType) {
+ ComponentType oldType = componentTypesByComponentName.put(componentId.getName(), componentType);
+ if (oldType != null) {
+ ensureTypesMatch(componentType, oldType, componentId.getName());
+ }
+ }
+
+ private void ensureNotDefinition(String componentName, Element componentSpec) {
+ if (componentSpec.getAttributes().getLength() > 1 || !XML.getChildren(componentSpec).isEmpty())
+ throw new RuntimeException("Expecting " + componentName +
+ " to be a reference to a global component with the same name," +
+ " so no additional attributes or nested elements are allowed");
+ }
+
+ private void ensureTypesMatch(ComponentType type1, ComponentType type2, String componentName) {
+ if (!type1.equals(type2)) {
+ throw new RuntimeException("Two different types declared for the component with name '" + componentName + "' ("
+ + type1.name + " != " + type2.name + ").");
+ }
+ }
+
+ private Map<String, ComponentType> unmodifiable(Map<String, ComponentType> outerComponentTypeByComponentName) {
+ return (outerComponentTypeByComponentName != null)?
+ Collections.unmodifiableMap(outerComponentTypeByComponentName):
+ Collections.<String, ComponentType>emptyMap();
+ }
+
+ public Collection<T> getComponentDefinitions() {
+ return Collections.unmodifiableCollection(componentDefinitions);
+ }
+
+ public Map<String, ComponentType> getComponentTypeByComponentName() {
+ return Collections.unmodifiableMap(componentTypesByComponentName);
+ }
+
+ public Set<ComponentSpecification> getOuterComponentReferences() {
+ return Collections.unmodifiableSet(outerComponentReferences);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilder.java
new file mode 100644
index 00000000000..85ed36d0356
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilder.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import org.w3c.dom.Element;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Builds Dependencies (provides, before, after) from an element.
+ * @author tonytv
+ */
+public class DependenciesBuilder {
+ private final Dependencies dependencies;
+
+ public DependenciesBuilder(Element spec) {
+
+ Set<String> provides = read(spec, "provides");
+ Set<String> before = read(spec, "before");
+ Set<String> after = read(spec, "after");
+
+ dependencies = new Dependencies(provides, before, after);
+ }
+
+ public Dependencies build() {
+ return dependencies;
+ }
+
+ private Set<String> read(Element spec, String name) {
+ Set<String> symbols = new HashSet<>();
+ symbols.addAll(XmlHelper.valuesFromElements(spec, name));
+ symbols.addAll(XmlHelper.spaceSeparatedSymbolsFromAttribute(spec, name));
+
+ return symbols;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomBuilderCreator.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomBuilderCreator.java
new file mode 100644
index 00000000000..1cf91f11ed1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomBuilderCreator.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Utility class for instantiating a builder using reflection.
+ * @author tonytv
+ */
+public class DomBuilderCreator {
+ public static <T> T create(Class<T> builderClass, Object... parameters) {
+ try {
+ return getConstructor(builderClass).newInstance(parameters);
+ } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> Constructor<T> getConstructor(Class<T> builderClass) {
+ Constructor<?>[] constructors = builderClass.getConstructors();
+ assert(constructors.length == 1);
+ return (Constructor<T>) constructors[0];
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainBuilderBase.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainBuilderBase.java
new file mode 100644
index 00000000000..3233424f4ac
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainBuilderBase.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Base functionality for all chain builders (docprocChain, searchChain, provider, source)
+ * @author tonytv
+ */
+public abstract class DomChainBuilderBase<COMPONENT extends ChainedComponent<?>, CHAIN extends Chain<COMPONENT>>
+ extends VespaDomBuilder.DomConfigProducerBuilder<CHAIN> {
+
+ private Collection<ComponentsBuilder.ComponentType<COMPONENT>> allowedComponentTypes;
+ protected final Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName;
+
+ public DomChainBuilderBase(Collection<ComponentsBuilder.ComponentType<COMPONENT>> allowedComponentTypes,
+ Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName) {
+ this.allowedComponentTypes = allowedComponentTypes;
+ this.outerComponentTypeByComponentName = outerComponentTypeByComponentName;
+ }
+
+ public final CHAIN doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ ComponentsBuilder<COMPONENT> componentsBuilder =
+ new ComponentsBuilder<>(ancestor, allowedComponentTypes, Arrays.asList(producerSpec), outerComponentTypeByComponentName);
+ ChainSpecification specWithoutInnerComponents =
+ new ChainSpecificationBuilder(producerSpec).build(componentsBuilder.getOuterComponentReferences());
+
+ CHAIN chain = buildChain(ancestor, producerSpec, specWithoutInnerComponents);
+ addInnerComponents(chain, componentsBuilder.getComponentDefinitions());
+
+ return chain;
+ }
+
+ private void addInnerComponents(CHAIN chain, Collection<COMPONENT> componentDefinitions) {
+ for (COMPONENT innerComponent : componentDefinitions) {
+ chain.addInnerComponent(innerComponent);
+ }
+ }
+
+ protected abstract CHAIN buildChain(AbstractConfigProducer ancestor, Element producerSpec,
+ ChainSpecification specWithoutInnerComponents);
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainsBuilder.java
new file mode 100644
index 00000000000..9555d916d2e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/DomChainsBuilder.java
@@ -0,0 +1,102 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.config.application.Xml;
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder.ComponentType;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import com.yahoo.vespa.model.container.component.chain.Chains;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * NOTE: This class _must_ be abstract, due to calling subclass method in ctor.
+ * @author tonytv
+ * @author gjoranv
+ */
+public abstract
+class DomChainsBuilder<COMPONENT extends ChainedComponent<?>, CHAIN extends Chain<COMPONENT>, CHAINS extends Chains<CHAIN>>
+ extends VespaDomBuilder.DomConfigProducerBuilder<CHAINS> {
+
+ private final Collection<ComponentType<COMPONENT>> allowedComponentTypes;
+ private final String appPkgChainsDir;
+ private final Element outerChainsElem;
+
+ protected DomChainsBuilder(Element outerChainsElem,
+ Collection<ComponentType<COMPONENT>> allowedComponentTypes,
+ String appPkgChainsDir) {
+
+ this.outerChainsElem = outerChainsElem;
+
+ // XXX: hack to prevent 'config' in the outer chains elem.
+ // TODO: Remove when this can be handled by schema, i.e. when we don't need the non-cluster qrservers syntax anymore
+ if (outerChainsElem != null) {
+ if (XML.getChildren(outerChainsElem, "config").size() > 0) {
+ throw new RuntimeException("At node " + XML.getNodePath(outerChainsElem, " > ") + ": " +
+ "'config' is not allowed in the outer chains element, please move it up one level!");
+ }
+ }
+
+ this.allowedComponentTypes = new ArrayList<>(allowedComponentTypes);
+ this.appPkgChainsDir = appPkgChainsDir;
+ }
+
+ protected abstract CHAINS newChainsInstance(AbstractConfigProducer parent);
+
+ @Override
+ protected final CHAINS doBuild(AbstractConfigProducer parent, Element chainsElement) {
+ CHAINS chains = newChainsInstance(parent);
+
+ List<Element> allChainElements = allChainElements(parent, chainsElement);
+ if (! allChainElements.isEmpty()) {
+ ComponentsBuilder<COMPONENT> outerComponentsBuilder = readOuterComponents(chains, allChainElements);
+ ChainsBuilder<COMPONENT, CHAIN> chainsBuilder = readChains(chains, allChainElements,
+ outerComponentsBuilder.getComponentTypeByComponentName());
+
+ addOuterComponents(chains, outerComponentsBuilder);
+ addChains(chains, chainsBuilder);
+ }
+ return chains;
+ }
+
+ private List<Element> allChainElements(AbstractConfigProducer ancestor, Element chainsElement) {
+ List<Element> chainsElements = new ArrayList<>();
+ if (outerChainsElem != null)
+ chainsElements.add(outerChainsElem);
+ chainsElements.add(chainsElement);
+
+ if (appPkgChainsDir != null)
+ chainsElements.addAll(Xml.allElemsFromPath(ancestor.getRoot().getDeployState().getApplicationPackage(), appPkgChainsDir));
+
+ return chainsElements;
+ }
+
+ private ComponentsBuilder<COMPONENT> readOuterComponents(AbstractConfigProducer ancestor, List<Element> chainsElems) {
+ return new ComponentsBuilder<>(ancestor, allowedComponentTypes, chainsElems, null);
+ }
+
+ protected abstract
+ ChainsBuilder<COMPONENT, CHAIN> readChains(AbstractConfigProducer ancestor, List<Element> allChainsElems,
+ Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName);
+
+ private void addOuterComponents(CHAINS chains, ComponentsBuilder<COMPONENT> outerComponentsBuilder) {
+ assert (outerComponentsBuilder.getOuterComponentReferences().isEmpty());
+
+ for (ChainedComponent outerComponent : outerComponentsBuilder.getComponentDefinitions()) {
+ chains.add(outerComponent);
+ }
+ }
+
+ private void addChains(CHAINS chains, ChainsBuilder<COMPONENT, CHAIN> chainsBuilder) {
+ for (CHAIN chain : chainsBuilder.getChains()) {
+ chains.add(chain);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/GenericChainedComponentModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/GenericChainedComponentModelBuilder.java
new file mode 100644
index 00000000000..a7f5a306f8b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/GenericChainedComponentModelBuilder.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import org.w3c.dom.Element;
+
+/**
+ * reads the common attributes and elements of all chained component elements.
+ * @author tonytv
+ */
+public abstract class GenericChainedComponentModelBuilder {
+ //The componentId might be used as a spec later(for example as class or
+ //bundle), so we must treat it as a specification until then.
+ protected final ComponentSpecification componentId;
+ protected final Dependencies dependencies;
+
+ public GenericChainedComponentModelBuilder(Element spec) {
+ componentId = readComponentId(spec);
+ dependencies = readDependencies(spec);
+ }
+
+ private Dependencies readDependencies(Element spec) {
+ return new DependenciesBuilder(spec).build();
+ }
+
+ protected ComponentSpecification readComponentId(Element spec) {
+ return XmlHelper.getIdRef(spec);
+ }
+
+ protected abstract ChainedComponentModel build();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/InheritanceBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/InheritanceBuilder.java
new file mode 100644
index 00000000000..e617ea93405
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/InheritanceBuilder.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.text.XML;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * Build an Inheritance object from an inheritance section.
+ * @author tonytv
+ */
+public class InheritanceBuilder {
+ final ChainSpecification.Inheritance inheritance;
+
+ public InheritanceBuilder(Element spec) {
+ inheritance = new ChainSpecification.Inheritance(
+ // XXX: for this to work, the tagname in the spec must match the tagname inside the 'inherits' elem, e.g. 'searchchain->inherits->searchchain'
+ read(spec, "inherits", spec.getTagName()),
+ read(spec, "excludes", "exclude"));
+ }
+
+ public ChainSpecification.Inheritance build() {
+ return inheritance;
+ }
+
+ private Set<ComponentSpecification> read(Element spec, String attributeName, String elementName) {
+ Set<ComponentSpecification> componentSpecifications = new LinkedHashSet<>();
+
+ componentSpecifications.addAll(spaceSeparatedComponentSpecificationsFromAttribute(spec, attributeName));
+
+ // TODO: the 'inherits' element is undocumented, and can be removed in an upcoming version of Vespa
+ componentSpecifications.addAll(idRefFromElements(XML.getChild(spec, "inherits"), elementName));
+
+
+ return componentSpecifications;
+ }
+
+ private Collection<ComponentSpecification> idRefFromElements(Element spec, String elementName) {
+ Collection<ComponentSpecification> result = new ArrayList<>();
+ if (spec == null)
+ return result;
+
+ for (Element element : XML.getChildren(spec, elementName)) {
+ result.add(XmlHelper.getIdRef(element));
+ }
+ return result;
+ }
+
+ private Collection<ComponentSpecification> spaceSeparatedComponentSpecificationsFromAttribute(Element spec, String attributeName) {
+ return toComponentSpecifications(XmlHelper.spaceSeparatedSymbolsFromAttribute(spec, attributeName));
+ }
+
+ private Set<ComponentSpecification> toComponentSpecifications(Collection<String> symbols) {
+ Set<ComponentSpecification> specifications = new LinkedHashSet<>();
+ for (String symbol : symbols) {
+ specifications.add(new ComponentSpecification(symbol));
+ }
+ return specifications;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocprocChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocprocChainsBuilder.java
new file mode 100644
index 00000000000..8aabea965eb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocprocChainsBuilder.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.docproc;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.container.docproc.DocumentProcessor;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * Creates all docproc chains from xml.
+ *
+ * @author gjoranv
+ */
+public class DocprocChainsBuilder extends ChainsBuilder<DocumentProcessor, DocprocChain> {
+
+ private static final Map<String, Class<? extends DomChainBuilderBase<? extends DocumentProcessor, ? extends DocprocChain>>>
+ chainType2builderClass = Collections.unmodifiableMap(
+ new LinkedHashMap<String, Class<? extends DomChainBuilderBase<? extends DocumentProcessor, ? extends DocprocChain>>>() {{
+ put("docprocchain", DomDocprocChainBuilder.class);
+ put("chain", DomDocprocChainBuilder.class);
+ }});
+
+ public DocprocChainsBuilder(AbstractConfigProducer ancestor, List<Element> docprocChainsElements,
+ Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+ super(ancestor, docprocChainsElements, outerSearcherTypeByComponentName, chainType2builderClass);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocumentProcessorModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocumentProcessorModelBuilder.java
new file mode 100644
index 00000000000..e2c5afb3c86
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DocumentProcessorModelBuilder.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.docproc;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainedComponentModelBuilder;
+import com.yahoo.vespa.model.container.docproc.model.DocumentProcessorModel;
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class DocumentProcessorModelBuilder extends ChainedComponentModelBuilder {
+
+ private Map<Pair<String, String>, String> fieldNameSchemaMap = new HashMap<>();
+
+ public DocumentProcessorModelBuilder(Element spec) {
+ super(spec);
+ readFieldNameSchemaMap(spec);
+ }
+
+ private void readFieldNameSchemaMap(Element spec) {
+ fieldNameSchemaMap = parseFieldNameSchemaMap(spec);
+ }
+
+ @Override
+ public DocumentProcessorModel build() {
+ return new DocumentProcessorModel(bundleInstantiationSpec, dependencies, fieldNameSchemaMap);
+ }
+
+ /**
+ * Parses a schemamapping element and generates a map of field mappings
+ *
+ * @param e a schemamapping element
+ * @return doctype,in-document → in-processor
+ */
+ public static Map<Pair<String,String>, String> parseFieldNameSchemaMap(Element e) {
+ Map<Pair<String, String>, String> ret = new HashMap<>();
+ for (Element sm : XML.getChildren(e, "map")) {
+ for (Element fm : XML.getChildren(sm, "field")) {
+ String from = fm.getAttribute("in-document");
+ String to = fm.getAttribute("in-processor");
+ String doctype = fm.getAttribute("doctype");
+ if ("".equals(doctype)) doctype=null;
+ ret.put(new Pair<>(doctype, from), to);
+ }
+ }
+ return ret;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainBuilder.java
new file mode 100644
index 00000000000..476275ca290
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainBuilder.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.docproc;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.container.docproc.DocumentProcessor;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Builds a docproc chain from xml
+ *
+ * @author gjoranv
+ */
+public class DomDocprocChainBuilder extends DomChainBuilderBase<DocumentProcessor, DocprocChain> {
+
+ public DomDocprocChainBuilder(Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName) {
+ super(Arrays.asList(ComponentsBuilder.ComponentType.documentprocessor), outerComponentTypeByComponentName);
+ }
+
+ protected DocprocChain buildChain(AbstractConfigProducer ancestor, Element producerSpec,
+ ChainSpecification specWithoutInnerComponents) {
+ Map<Pair<String, String>, String> fieldNameSchemaMap = DocumentProcessorModelBuilder.parseFieldNameSchemaMap(producerSpec);
+ return new DocprocChain(specWithoutInnerComponents, fieldNameSchemaMap);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainsBuilder.java
new file mode 100644
index 00000000000..07dc6e337fd
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocprocChainsBuilder.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.docproc;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder.ComponentType;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainsBuilder;
+import com.yahoo.vespa.model.container.docproc.DocprocChains;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.container.docproc.DocumentProcessor;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Builds the docproc chains model from xml.
+ *
+ * @author gjoranv
+ */
+public class DomDocprocChainsBuilder extends DomChainsBuilder<DocumentProcessor, DocprocChain, DocprocChains> {
+ public DomDocprocChainsBuilder(Element outerChainsElem, boolean supportDocprocChainsDir) {
+ super(outerChainsElem, Arrays.asList(ComponentType.documentprocessor),
+ supportDocprocChainsDir ? ApplicationPackage.DOCPROCCHAINS_DIR: null);
+ }
+
+ @Override
+ protected DocprocChains newChainsInstance(AbstractConfigProducer parent) {
+ return new DocprocChains(parent, "docprocchains");
+ }
+
+ @Override
+ protected DocprocChainsBuilder readChains(AbstractConfigProducer ancestor, List<Element> docprocChainsElements,
+ Map<String, ComponentType> outerComponentTypeByComponentName) {
+ return new DocprocChainsBuilder(ancestor, docprocChainsElements, outerComponentTypeByComponentName);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocumentProcessorBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocumentProcessorBuilder.java
new file mode 100644
index 00000000000..43b652ccff5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/docproc/DomDocumentProcessorBuilder.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.docproc;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.docproc.DocumentProcessor;
+import org.w3c.dom.Element;
+
+/**
+ * Builds a DocumentProcessor from XML.
+ *
+ * @author gjoranv
+ */
+public class DomDocumentProcessorBuilder extends VespaDomBuilder.DomConfigProducerBuilder<DocumentProcessor> {
+
+ protected DocumentProcessor doBuild(AbstractConfigProducer ancestor, Element documentProcessorElement) {
+ DocumentProcessorModelBuilder modelBuilder = new DocumentProcessorModelBuilder(documentProcessorElement);
+ return new DocumentProcessor(modelBuilder.build());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingBuilder.java
new file mode 100644
index 00000000000..4533b078b28
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingBuilder.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.processing;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainsBuilder;
+import com.yahoo.vespa.model.container.processing.ProcessingChain;
+import com.yahoo.vespa.model.container.processing.ProcessingChains;
+import com.yahoo.vespa.model.container.processing.Processor;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Root builder of the processing model
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.6
+ */
+public class DomProcessingBuilder extends DomChainsBuilder<Processor, ProcessingChain, ProcessingChains> {
+
+ public DomProcessingBuilder(Element outerChainsElem) {
+ super(outerChainsElem, Arrays.asList(ComponentsBuilder.ComponentType.processor), ApplicationPackage.PROCESSORCHAINS_DIR);
+ }
+
+ @Override
+ protected ProcessingChains newChainsInstance(AbstractConfigProducer parent) {
+ return new ProcessingChains(parent, "processing");
+ }
+
+ @Override
+ protected ProcessingChainsBuilder readChains(AbstractConfigProducer ancestor, List<Element> processingChainsElements,
+ Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName) {
+ return new ProcessingChainsBuilder(ancestor, processingChainsElements, outerComponentTypeByComponentName);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingChainBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingChainBuilder.java
new file mode 100644
index 00000000000..a3cbff2910f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessingChainBuilder.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.processing;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.processing.ProcessingChain;
+import com.yahoo.vespa.model.container.processing.Processor;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.6
+ */
+public class DomProcessingChainBuilder extends DomChainBuilderBase<Processor, ProcessingChain> {
+
+ public DomProcessingChainBuilder(Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName) {
+ super(Arrays.asList(ComponentsBuilder.ComponentType.processor), outerComponentTypeByComponentName);
+ }
+
+ protected ProcessingChain buildChain(AbstractConfigProducer ancestor, Element producerSpec,
+ ChainSpecification specWithoutInnerComponents) {
+ return new ProcessingChain(specWithoutInnerComponents);
+ }
+
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessorBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessorBuilder.java
new file mode 100644
index 00000000000..b48d3dcab2f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/DomProcessorBuilder.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.processing;
+
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainedComponentModelBuilder;
+import com.yahoo.vespa.model.container.processing.Processor;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import org.w3c.dom.Element;
+
+/**
+ * Builds a processor from XML.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.6
+ */
+public class DomProcessorBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Processor> {
+
+ protected Processor doBuild(AbstractConfigProducer ancestor, Element processorElement) {
+ ChainedComponentModelBuilder modelBuilder = new ChainedComponentModelBuilder(processorElement);
+ return new Processor(modelBuilder.build());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/ProcessingChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/ProcessingChainsBuilder.java
new file mode 100644
index 00000000000..5806de9343e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/processing/ProcessingChainsBuilder.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.processing;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.processing.ProcessingChain;
+import com.yahoo.vespa.model.container.processing.Processor;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * Creates all processing chains from xml.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.6
+ */
+public class ProcessingChainsBuilder extends ChainsBuilder<Processor, ProcessingChain> {
+
+ private static final Map<String, Class<? extends DomChainBuilderBase<? extends Processor, ? extends ProcessingChain>>>
+ chainType2builderClass = Collections.unmodifiableMap(
+ new LinkedHashMap<String, Class<? extends DomChainBuilderBase<? extends Processor, ? extends ProcessingChain>>>() {{
+ put("chain", DomProcessingChainBuilder.class);
+ }});
+
+ public ProcessingChainsBuilder(AbstractConfigProducer ancestor, List<Element> processingChainsElements,
+ Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+ super(ancestor, processingChainsElements, outerSearcherTypeByComponentName, chainType2builderClass);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilder.java
new file mode 100644
index 00000000000..c06d62b7d43
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilder.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.search.searchchain.model.federation.FederationSearcherModel;
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.DomComponentBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.GenericChainedComponentModelBuilder;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.search.searchchain.FederationSearcher;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import org.w3c.dom.Element;
+import scala.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Builds a federation searcher config producer from an element.
+ * @author tonytv
+ */
+public class DomFederationSearcherBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Searcher<?>> {
+ static class FederationSearcherModelBuilder extends GenericChainedComponentModelBuilder {
+ private final List<FederationSearcherModel.TargetSpec> sources;
+ private final boolean inheritDefaultSources;
+
+ FederationSearcherModelBuilder(Element searcherSpec) {
+ super(searcherSpec);
+ sources = readSources(searcherSpec);
+ inheritDefaultSources = readSourceSet(searcherSpec);
+ }
+
+ private boolean readSourceSet(Element searcherSpec) {
+ return XML.getChild(searcherSpec, "source-set") != null;
+ }
+
+
+ private List<FederationSearcherModel.TargetSpec> readSources(Element searcherSpec) {
+ List<FederationSearcherModel.TargetSpec> sources = new ArrayList<>();
+ for (Element source : XML.getChildren(searcherSpec, "source")) {
+ sources.add(readSource(source));
+ }
+ return sources;
+ }
+
+ private FederationSearcherModel.TargetSpec readSource(Element source) {
+ ComponentSpecification componentSpecification = XmlHelper.getIdRef(source);
+
+ FederationOptions federationOptions =
+ readFederationOptions(XML.getChild(source, FederationOptionsBuilder.federationOptionsElement));
+
+ return new FederationSearcherModel.TargetSpec(componentSpecification, federationOptions);
+ }
+
+ private FederationOptions readFederationOptions(Element federationOptionsElement) {
+ if (federationOptionsElement == null) {
+ return new FederationOptions();
+ } else {
+ return new FederationOptionsBuilder(federationOptionsElement).build();
+ }
+ }
+
+ protected FederationSearcherModel build() {
+ return new FederationSearcherModel(componentId, dependencies, sources, inheritDefaultSources);
+ }
+ }
+
+ protected FederationSearcher doBuild(AbstractConfigProducer ancestor, Element searcherElement) {
+ FederationSearcherModel model = new FederationSearcherModelBuilder(searcherElement).build();
+ Optional<Component> targetSelector = buildTargetSelector(ancestor, searcherElement, model.getComponentId());
+
+ return new FederationSearcher(model, targetSelector);
+ }
+
+ private Optional<Component> buildTargetSelector(AbstractConfigProducer ancestor, Element searcherElement, ComponentId namespace) {
+ Element targetSelectorElement = XML.getChild(searcherElement, "target-selector");
+ if (targetSelectorElement == null)
+ return Optional.empty();
+
+ return Optional.of(new DomComponentBuilder(namespace).build(ancestor, targetSelectorElement));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomGenericTargetBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomGenericTargetBuilder.java
new file mode 100644
index 00000000000..59ecbe99b40
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomGenericTargetBuilder.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.search.searchchain.GenericTarget;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ * Base functionality for all target chain builders (provider, source)
+ */
+abstract public class DomGenericTargetBuilder<T extends GenericTarget> extends DomChainBuilderBase<Searcher<?>, T> {
+
+ DomGenericTargetBuilder(Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+ super(Arrays.asList(ComponentsBuilder.ComponentType.searcher, ComponentsBuilder.ComponentType.federation),
+ outerSearcherTypeByComponentName);
+ }
+
+ protected static FederationOptions readFederationOptions(Element sourceElement) {
+ Element optionsElement = XML.getChild(sourceElement, FederationOptionsBuilder.federationOptionsElement);
+ if (optionsElement == null) {
+ return new FederationOptions();
+ } else {
+ return new FederationOptionsBuilder(optionsElement).build();
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilder.java
new file mode 100644
index 00000000000..bfd7dbc3a3c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilder.java
@@ -0,0 +1,283 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.binaryprefix.BinaryPrefix;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.search.searchchain.model.federation.HttpProviderSpec;
+import com.yahoo.search.searchchain.model.federation.LocalProviderSpec;
+import com.yahoo.text.XML;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.BinaryScaledAmountParser;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.container.search.searchchain.HttpProvider;
+import com.yahoo.vespa.model.container.search.searchchain.HttpProviderSearcher;
+import com.yahoo.vespa.model.container.search.searchchain.LocalProvider;
+import com.yahoo.vespa.model.container.search.searchchain.Provider;
+import com.yahoo.vespa.model.container.search.searchchain.Source;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Builds a provider from xml.
+ * The demangling of provider types is taken care of here,
+ * since the mangling is an intrinsic of the configuration language,
+ * not the model itself.
+ *
+ * @author tonytv
+ */
+public class DomProviderBuilder extends DomGenericTargetBuilder<Provider> {
+ /**
+ * Retrieves all possible provider specific parameters
+ */
+ private static class ProviderReader {
+ final String type;
+ final String path;
+ final Double cacheWeight;
+ final Integer retries;
+ final Double readTimeout;
+ final Double connectionTimeout;
+ final Double connectionPoolTimeout;
+ final String clusterName;
+ final List<HttpProviderSpec.Node> nodes;
+ final String ycaApplicationId;
+ final Integer ycaTtl;
+ final Integer ycaRetryWait;
+ final HttpProviderSpec.Node ycaProxy; // Just re-using the Node class, as it matches our needs
+ final Integer cacheSizeMB;
+
+ ProviderReader(Element providerElement) {
+ type = readType(providerElement);
+ path = readPath(providerElement);
+ cacheWeight = readCacheWeight(providerElement);
+ cacheSizeMB = readCacheSize(providerElement);
+ clusterName = readCluster(providerElement);
+ readTimeout = readReadTimeout(providerElement);
+ connectionTimeout = readConnectionTimeout(providerElement);
+ connectionPoolTimeout = readConnectionPoolTimeout(providerElement);
+ retries = readRetries(providerElement);
+ nodes = readNodes(providerElement);
+ ycaApplicationId = readYcaApplicationId(providerElement);
+ ycaTtl = readYcaTtl(providerElement);
+ ycaRetryWait = readYcaRetryWait(providerElement);
+ ycaProxy = readYcaProxy(providerElement);
+ }
+
+
+ private String getAttributeOrNull(Element element, String name) {
+ String value = element.getAttribute(name);
+ return value.isEmpty() ? null : value;
+ }
+
+ private String readPath(Element providerElement) {
+ return getAttributeOrNull(providerElement, "path");
+ }
+
+ private String readCluster(Element providerElement) {
+ return getAttributeOrNull(providerElement, "cluster");
+ }
+
+ private Double readCacheWeight(Element providerElement) {
+ String cacheWeightString = getAttributeOrNull(providerElement, "cacheweight");
+ return (cacheWeightString == null)? null : Double.parseDouble(cacheWeightString);
+ }
+
+ private Integer readCacheSize(Element providerElement) {
+ String cacheSize = getAttributeOrNull(providerElement, "cachesize");
+ return (cacheSize == null)? null : (int)BinaryScaledAmountParser.parse(cacheSize).as(BinaryPrefix.mega);
+ }
+
+ private Integer readRetries(Element providerElement) {
+ String retriesString = getAttributeOrNull(providerElement, "retries");
+ return (retriesString == null) ? null : Integer.parseInt(retriesString);
+ }
+
+ private Double readReadTimeout(Element providerElement) {
+ String timeoutString = getAttributeOrNull(providerElement, "readtimeout");
+ return (timeoutString == null) ? null : TimeParser.seconds(timeoutString);
+ }
+
+ private Double readConnectionTimeout(Element providerElement) {
+ String timeoutString = getAttributeOrNull(providerElement, "connectiontimeout");
+ return (timeoutString == null) ? null : TimeParser.seconds(timeoutString);
+ }
+
+ private Double readConnectionPoolTimeout(Element providerElement) {
+ String timeoutString = getAttributeOrNull(providerElement, "connectionpooltimeout");
+ return (timeoutString == null) ? null : TimeParser.seconds(timeoutString);
+ }
+
+ private String readYcaApplicationId(Element providerElement) {
+ return getAttributeOrNull(providerElement, "yca-application-id");
+ }
+
+ private Integer readYcaTtl(Element providerElement) {
+ String x = getAttributeOrNull(providerElement, "yca-cache-ttl");
+ return (x == null) ? null : TimeParser.seconds(x).intValue();
+ }
+
+ private Integer readYcaRetryWait(Element providerElement) {
+ String x = getAttributeOrNull(providerElement, "yca-cache-retry-wait");
+ return (x == null) ? null : TimeParser.seconds(x).intValue();
+ }
+
+ private HttpProviderSpec.Node readYcaProxy(Element providerElement) {
+ Element ycaProxySpec = XML.getChild(providerElement, "yca-proxy");
+ if (ycaProxySpec == null) {
+ return null; // no proxy
+ }
+ if(getAttributeOrNull(ycaProxySpec, "host") == null) {
+ return new HttpProviderSpec.Node(null, 0); // default proxy
+ }
+ return readNode(ycaProxySpec);
+ }
+
+ private List<HttpProviderSpec.Node> readNodes(Element providerElement) {
+ Element nodesSpec = XML.getChild(providerElement, "nodes");
+ if (nodesSpec == null) {
+ return null;
+ }
+
+ List<HttpProviderSpec.Node> nodes = new ArrayList<>();
+ for (Element nodeSpec : XML.getChildren(nodesSpec, "node")) {
+ nodes.add(readNode(nodeSpec));
+ }
+ return nodes;
+ }
+
+ private HttpProviderSpec.Node readNode(Element nodeElement) {
+ String host = getAttributeOrNull(nodeElement, "host");
+ // The direct calls to parse methods below works because the schema
+ // guarantees us no null references
+ int port = Integer.parseInt(getAttributeOrNull(nodeElement, "port"));
+ return new HttpProviderSpec.Node(host, port);
+ }
+
+ private String readType(Element providerElement) {
+ return getAttributeOrNull(providerElement, "type");
+ }
+ }
+
+ public DomProviderBuilder(Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+ super(outerSearcherTypeByComponentName);
+ }
+
+ @Override
+ protected Provider buildChain(AbstractConfigProducer ancestor, Element providerElement,
+ ChainSpecification specWithoutInnerComponents) {
+
+ ProviderReader providerReader = new ProviderReader(providerElement);
+ if (providerReader.ycaApplicationId == null && providerReader.ycaProxy != null) {
+ throw new IllegalArgumentException(
+ "Provider '" + specWithoutInnerComponents.componentId +
+ "' must have a YCA application ID, since a YCA proxy is given");
+ }
+
+ FederationOptions federationOptions = readFederationOptions(providerElement);
+
+ Provider provider = buildProvider(specWithoutInnerComponents, providerReader, federationOptions);
+
+ Collection<Source> sources = buildSources(ancestor, providerElement);
+ addSources(provider, sources);
+
+ return provider;
+ }
+
+
+ private Collection<Source> buildSources(AbstractConfigProducer ancestor, Element providerElement) {
+ List<Source> sources = new ArrayList<>();
+ for (Element sourceElement : XML.getChildren(providerElement, "source")) {
+ sources.add(new DomSourceBuilder(outerComponentTypeByComponentName).build(ancestor, sourceElement));
+ }
+ return sources;
+ }
+
+ private void addSources(Provider provider, Collection<Source> sources) {
+ for (Source source : sources) {
+ provider.addSource(source);
+ }
+ }
+
+ private Provider buildProvider(ChainSpecification specWithoutInnerSearchers,
+ ProviderReader providerReader, FederationOptions federationOptions) {
+
+ if (providerReader.type == null) {
+ return buildEmptyHttpProvider(specWithoutInnerSearchers, providerReader, federationOptions);
+ } else if (HttpProviderSpec.includesType(providerReader.type)) {
+ return buildHttpProvider(specWithoutInnerSearchers, providerReader, federationOptions);
+ } else if (LocalProviderSpec.includesType(providerReader.type)) {
+ return buildLocalProvider(specWithoutInnerSearchers, providerReader, federationOptions);
+ } else {
+ throw new RuntimeException("Unknown provider type '" + providerReader.type + "'");
+ }
+ }
+
+ private Provider buildLocalProvider(ChainSpecification specWithoutInnerSearchers, ProviderReader providerReader, FederationOptions federationOptions) {
+ try {
+ ensureEmpty(specWithoutInnerSearchers.componentId, providerReader.cacheWeight, providerReader.path, providerReader.nodes,
+ providerReader.readTimeout, providerReader.connectionTimeout, providerReader.connectionPoolTimeout,
+ providerReader.retries, providerReader.ycaApplicationId, providerReader.ycaTtl,
+ providerReader.ycaRetryWait, providerReader.ycaProxy);
+
+ return new LocalProvider(specWithoutInnerSearchers,
+ federationOptions,
+ new LocalProviderSpec(providerReader.clusterName, providerReader.cacheSizeMB));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed creating local provider " + specWithoutInnerSearchers.componentId, e);
+ }
+ }
+
+ private Provider buildHttpProvider(ChainSpecification specWithoutInnerSearchers, ProviderReader providerReader, FederationOptions federationOptions) {
+ ensureEmpty(specWithoutInnerSearchers.componentId, providerReader.clusterName);
+
+ Provider httpProvider = buildEmptyHttpProvider(specWithoutInnerSearchers, providerReader, federationOptions);
+
+ httpProvider.addInnerComponent(new HttpProviderSearcher(
+ new ChainedComponentModel(
+ HttpProviderSpec.toBundleInstantiationSpecification(HttpProviderSpec.Type.valueOf(providerReader.type)),
+ Dependencies.emptyDependencies())));
+
+ return httpProvider;
+ }
+
+
+ private Provider buildEmptyHttpProvider(ChainSpecification specWithoutInnerSearchers, ProviderReader providerReader, FederationOptions federationOptions) {
+ ensureEmpty(specWithoutInnerSearchers.componentId, providerReader.clusterName);
+
+ return new HttpProvider(specWithoutInnerSearchers,
+ federationOptions,
+ new HttpProviderSpec(
+ providerReader.cacheWeight,
+ providerReader.path,
+ providerReader.nodes,
+ providerReader.ycaApplicationId,
+ providerReader.ycaTtl,
+ providerReader.ycaRetryWait,
+ providerReader.ycaProxy,
+ providerReader.cacheSizeMB,
+ connectionParameters(providerReader)));
+ }
+
+ private HttpProviderSpec.ConnectionParameters connectionParameters(ProviderReader providerReader) {
+ return new HttpProviderSpec.ConnectionParameters(
+ providerReader.readTimeout,
+ providerReader.connectionTimeout,
+ providerReader.connectionPoolTimeout,
+ providerReader.retries);
+ }
+
+ private void ensureEmpty(ComponentId componentId, Object... objects) {
+ for (Object object : objects) {
+ if (object != null) {
+ throw new RuntimeException("Invalid provider option in provider '" + componentId + "': value='" + object + "'");
+ }
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainBuilder.java
new file mode 100644
index 00000000000..d79ea1a536b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainBuilder.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChain;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Builds a Search chain from xml.
+ * @author tonytv
+ */
+public class DomSearchChainBuilder extends DomChainBuilderBase<Searcher<?>, SearchChain> {
+
+ public DomSearchChainBuilder(Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+ super(Arrays.asList(ComponentsBuilder.ComponentType.searcher, ComponentsBuilder.ComponentType.federation),
+ outerSearcherTypeByComponentName);
+ }
+
+ protected SearchChain buildChain(AbstractConfigProducer ancestor, Element producerSpec,
+ ChainSpecification specWithoutInnerComponents) {
+ return new SearchChain(specWithoutInnerComponents);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilder.java
new file mode 100644
index 00000000000..5cadc8cc271
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilder.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder.ComponentType;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainsBuilder;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChain;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Builds the search chains model from xml.
+ *
+ * @author tonytv
+ */
+public class DomSearchChainsBuilder extends DomChainsBuilder<Searcher<?>, SearchChain, SearchChains> {
+
+ public DomSearchChainsBuilder(Element outerChainsElem, boolean supportSearchChainsDir) {
+ super(outerChainsElem, Arrays.asList(ComponentType.searcher, ComponentType.federation),
+ supportSearchChainsDir ? ApplicationPackage.SEARCHCHAINS_DIR: null);
+ }
+
+ // For unit testing without outer chains
+ public DomSearchChainsBuilder() {
+ this(null, false);
+ }
+
+ @Override
+ protected SearchChains newChainsInstance(AbstractConfigProducer parent) {
+ return new SearchChains(parent, "searchchains");
+ }
+
+ @Override
+ protected SearchChainsBuilder readChains(AbstractConfigProducer ancestor, List<Element> searchChainsElements,
+ Map<String, ComponentType> outerComponentTypeByComponentName) {
+ return new SearchChainsBuilder(ancestor, searchChainsElements, outerComponentTypeByComponentName);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilder.java
new file mode 100644
index 00000000000..44f4e83b1c8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilder.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainedComponentModelBuilder;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import org.w3c.dom.Element;
+
+/**
+ * Builds a Searcher from XML.
+ * @author tonytv
+ */
+public class DomSearcherBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Searcher<?>> {
+
+ protected Searcher<ChainedComponentModel> doBuild(AbstractConfigProducer ancestor, Element searcherElement) {
+ ChainedComponentModelBuilder modelBuilder = new ChainedComponentModelBuilder(searcherElement);
+ return new Searcher<>(modelBuilder.build());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSourceBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSourceBuilder.java
new file mode 100644
index 00000000000..090ac6a6df3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSourceBuilder.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.container.search.searchchain.Source;
+import org.w3c.dom.Element;
+
+import java.util.Map;
+
+/**
+ * Builds a source from xml.
+ * @author tonytv
+ */
+public class DomSourceBuilder extends DomGenericTargetBuilder<Source> {
+ DomSourceBuilder(Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+ super(outerSearcherTypeByComponentName);
+ }
+
+ protected Source buildChain(AbstractConfigProducer ancestor, Element producerSpec, ChainSpecification specWithoutInnerComponents) {
+ Source.GroupOption groupOption =
+ XmlHelper.isReference(producerSpec) ?
+ Source.GroupOption.participant :
+ Source.GroupOption.leader;
+
+ return new Source(specWithoutInnerComponents, readFederationOptions(producerSpec), groupOption);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/FederationOptionsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/FederationOptionsBuilder.java
new file mode 100644
index 00000000000..ba84e66cb97
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/FederationOptionsBuilder.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import org.w3c.dom.Element;
+
+
+/**
+ * Builds federation options from a federations options element
+ * @author tonytv
+ */
+public class FederationOptionsBuilder {
+ public static final String federationOptionsElement = "federationoptions";
+
+ private final FederationOptions federationOptions;
+
+ FederationOptionsBuilder(Element spec) {
+ federationOptions =
+ new FederationOptions().
+ setUseByDefault(readUseByDefault(spec)).
+ setOptional(readOptional(spec)).
+ setTimeoutInMilliseconds(readTimeout(spec)).
+ setRequestTimeoutInMilliseconds(readRequestTimeout(spec));
+ }
+
+
+ private Integer readTimeout(Element spec) {
+ String timeout = spec.getAttribute("timeout");
+
+ return (timeout.isEmpty())?
+ null :
+ TimeParser.asMilliSeconds(timeout);
+ }
+
+ private Integer readRequestTimeout(Element spec) {
+ String requestTimeout = spec.getAttribute("requestTimeout");
+
+ return (requestTimeout.isEmpty())?
+ null :
+ TimeParser.asMilliSeconds(requestTimeout);
+ }
+
+ private Boolean readOptional(Element spec) {
+ String optional = spec.getAttribute("optional");
+ return (optional.isEmpty()) ?
+ null :
+ Boolean.parseBoolean(optional);
+ }
+
+ private Boolean readUseByDefault(Element spec) {
+ String useByDefault = spec.getAttribute("default");
+ return (useByDefault.isEmpty()) ?
+ null :
+ Boolean.parseBoolean(useByDefault);
+ }
+
+ FederationOptions build() {
+ return federationOptions;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/SearchChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/SearchChainsBuilder.java
new file mode 100644
index 00000000000..9b4a8908afe
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/SearchChainsBuilder.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChain;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ * Creates top level search chains(searchchain, provider) from xml.
+ */
+public class SearchChainsBuilder extends ChainsBuilder<Searcher<?>, SearchChain> {
+
+ private static final Map<String, Class<? extends DomChainBuilderBase<? extends Searcher<?>, ? extends SearchChain>>>
+ chainType2builderClass = Collections.unmodifiableMap(
+ new LinkedHashMap<String, Class<? extends DomChainBuilderBase<? extends Searcher<?>, ? extends SearchChain>>>() {{
+ put("chain", DomSearchChainBuilder.class);
+ put("searchchain", DomSearchChainBuilder.class);
+ put("provider", DomProviderBuilder.class);
+ }});
+
+ public SearchChainsBuilder(AbstractConfigProducer ancestor, List<Element> searchChainsElements,
+ Map<String, ComponentsBuilder.ComponentType> outerSearcherTypeByComponentName) {
+ super(ancestor, searchChainsElements, outerSearcherTypeByComponentName, chainType2builderClass);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/TimeParser.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/TimeParser.java
new file mode 100644
index 00000000000..7aa84d3a14b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/TimeParser.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for parsing timeout fields.
+ *
+ * @author tonytv
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class TimeParser {
+ private static final Pattern timeoutPattern = Pattern.compile("(\\d+(\\.\\d*)?)\\s*(m)?s");
+ private static final double milliSecondsPerSecond = 1000.0d;
+
+ public static Double seconds(String timeout) {
+ Matcher matcher = timeoutPattern.matcher(timeout);
+ if (!matcher.matches()) {
+ throw new RuntimeException("Timeout pattern not in sync with schema");
+ }
+
+ double value = Double.parseDouble(matcher.group(1));
+ if (matcher.group(3) != null) {
+ value /= milliSecondsPerSecond;
+ }
+ return new Double(value);
+ }
+
+ public static int asMilliSeconds(String timeout) {
+ Matcher matcher = timeoutPattern.matcher(timeout);
+ if (!matcher.matches()) {
+ throw new RuntimeException("Timeout pattern not in sync with schema");
+ }
+
+ double value = Double.parseDouble(matcher.group(1));
+ if (matcher.group(3) == null) {
+ value *= milliSecondsPerSecond;
+ }
+
+ return (int) value;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/package-info.java
new file mode 100644
index 00000000000..e51d7e4acee
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/clients/Clients.java b/config-model/src/main/java/com/yahoo/vespa/model/clients/Clients.java
new file mode 100644
index 00000000000..799d0b437ae
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/clients/Clients.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients;
+
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.documentapi.messagebus.loadtypes.LoadType;
+import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.search.ContainerHttpGateway;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * This is the clients plugin for the Vespa model. It is responsible for creating
+ * all clients services.
+ *
+ * @author <a href="mailto:gunnarga@yahoo-inc.com">Gunnar Gauslaa Bergem</a>
+ */
+public class Clients extends ConfigModel {
+
+ private static final long serialVersionUID = 1L;
+ private ContainerCluster containerHttpGateways = null;
+ private List<VespaSpoolerService> vespaSpoolers = new LinkedList<>();
+ private LoadTypeSet loadTypes = new LoadTypeSet();
+ private final AbstractConfigProducer parent;
+
+ public Clients(ConfigModelContext modelContext) {
+ super(modelContext);
+ this.parent = modelContext.getParentProducer();
+ }
+
+ public AbstractConfigProducer getConfigProducer() {
+ return parent;
+ }
+
+ @Override
+ public void prepare(ConfigModelRepo configModelRepo) {
+ if (containerHttpGateways != null) {
+ containerHttpGateways.prepare();
+ }
+ }
+
+ public void setContainerHttpGateways(ContainerCluster containerHttpGateways) {
+ this.containerHttpGateways = containerHttpGateways;
+ }
+
+ public List<VespaSpoolerService> getVespaSpoolers() {
+ return vespaSpoolers;
+ }
+
+ public LoadTypeSet getLoadTypes() {
+ return loadTypes;
+ }
+
+ public void getConfig(LoadTypeConfig.Builder builder) {
+ for (LoadType t : loadTypes.getNameMap().values()) {
+ if (t != LoadType.DEFAULT) {
+ builder.type(getLoadTypeConfig(t));
+ }
+ }
+ }
+
+ private LoadTypeConfig.Type.Builder getLoadTypeConfig(LoadType loadType) {
+ LoadTypeConfig.Type.Builder builder = new LoadTypeConfig.Type.Builder();
+ builder.name(loadType.getName());
+ builder.id(loadType.getId());
+ builder.priority(loadType.getPriority().toString());
+ return builder;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/clients/ContainerDocumentApi.java b/config-model/src/main/java/com/yahoo/vespa/model/clients/ContainerDocumentApi.java
new file mode 100644
index 00000000000..55cfc8b2fba
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/clients/ContainerDocumentApi.java
@@ -0,0 +1,165 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Handler;
+import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
+import com.yahoo.vespa.model.container.search.ContainerSearch;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChain;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import com.yahoo.vespa.model.container.search.searchchain.Searcher;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.11
+ */
+public class ContainerDocumentApi implements FeederConfig.Producer {
+
+ public static final String vespaClientBundleSpecification = "vespaclient-container-plugin";
+ private final Options options;
+
+ public ContainerDocumentApi(ContainerCluster cluster, Options options) {
+ this.options = options;
+ setupLegacySearchers(cluster);
+ setupHandlers(cluster);
+ }
+
+ private void setupHandlers(ContainerCluster cluster) {
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandler", "feed"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerRemove", "remove"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation", "removelocation"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerGet", "get"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerVisit", "visit"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.document.restapi.resource.RestApi", "document/v1/*"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerCompatibility", "document"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.feedhandler.VespaFeedHandlerStatus", "feedstatus"));
+ cluster.addComponent(newVespaClientHandler("com.yahoo.vespa.http.server.FeedHandler", ContainerCluster.RESERVED_URI_PREFIX + "/feedapi"));
+ }
+
+ private void setupLegacySearchers(ContainerCluster cluster) {
+ Set<ComponentSpecification> inherited = new TreeSet<>();
+
+ SearchChain vespaGetChain = new SearchChain(new ChainSpecification(new ComponentId("vespaget"),
+ new ChainSpecification.Inheritance(inherited, null), new ArrayList<Phase>(), new TreeSet<ComponentSpecification>()));
+ vespaGetChain.addInnerComponent(newVespaClientSearcher("com.yahoo.storage.searcher.GetSearcher"));
+
+ SearchChain vespaVisitChain = new SearchChain(new ChainSpecification(new ComponentId("vespavisit"),
+ new ChainSpecification.Inheritance(inherited, null), new ArrayList<Phase>(), new TreeSet<ComponentSpecification>()));
+ vespaVisitChain.addInnerComponent(newVespaClientSearcher("com.yahoo.storage.searcher.VisitSearcher"));
+
+ SearchChains chains;
+ if (cluster.getSearch() != null) {
+ chains = cluster.getSearchChains();
+ } else {
+ chains = new SearchChains(cluster, "searchchain");
+ }
+ chains.add(vespaGetChain);
+ chains.add(vespaVisitChain);
+
+ if (cluster.getSearch() == null) {
+ ContainerSearch containerSearch = new ContainerSearch(cluster, chains, new ContainerSearch.Options());
+ cluster.setSearch(containerSearch);
+
+ final ProcessingHandler<SearchChains> searchHandler = new ProcessingHandler<>(
+ chains, "com.yahoo.search.handler.SearchHandler");
+ searchHandler.addServerBindings("http://*/search/*", "https://*/search/*");
+ cluster.addComponent(searchHandler);
+ }
+ }
+
+ private Handler newVespaClientHandler(String componentId, String bindingSuffix) {
+ Handler<AbstractConfigProducer<?>> handler = new Handler<>(new ComponentModel(
+ BundleInstantiationSpecification.getFromStrings(componentId, null, vespaClientBundleSpecification), ""));
+
+ for (String rootBinding : options.bindings) {
+ handler.addServerBindings(rootBinding + bindingSuffix,
+ rootBinding + bindingSuffix + '/');
+ }
+ return handler;
+ }
+
+ private Searcher newVespaClientSearcher(String componentSpec) {
+ return new Searcher<>(new ChainedComponentModel(
+ BundleInstantiationSpecification.getFromStrings(componentSpec, null, vespaClientBundleSpecification),
+ new Dependencies(null, null, null)));
+ }
+
+ @Override
+ public void getConfig(FeederConfig.Builder builder) {
+ if (options.abortondocumenterror != null)
+ builder.abortondocumenterror(options.abortondocumenterror);
+ if (options.route!= null)
+ builder.route(options.route);
+ if (options.maxpendingdocs != null)
+ builder.maxpendingdocs(options.maxpendingdocs);
+ if (options.maxpendingbytes != null)
+ builder.maxpendingbytes(options.maxpendingbytes);
+ if (options.retryenabled != null)
+ builder.retryenabled(options.retryenabled);
+ if (options.retrydelay != null)
+ builder.retrydelay(options.retrydelay);
+ if (options.timeout != null)
+ builder.timeout(options.timeout);
+ if (options.tracelevel != null)
+ builder.tracelevel(options.tracelevel);
+ if (options.mbusport != null)
+ builder.mbusport(options.mbusport);
+ if (options.docprocChain != null)
+ builder.docprocchain(options.docprocChain);
+ }
+
+ public static final class Options {
+ private final Collection<String> bindings;
+ private final Boolean abortondocumenterror;
+ private final String route;
+ private final Integer maxpendingdocs;
+ private final Integer maxpendingbytes;
+ private final Boolean retryenabled;
+ private final Double retrydelay;
+ private final Double timeout;
+ private final Integer tracelevel;
+ private final Integer mbusport;
+ private final String docprocChain;
+
+ public Options(Collection<String> bindings,
+ Boolean abortondocumenterror,
+ String route,
+ Integer maxpendingdocs,
+ Integer maxpendingbytes,
+ Boolean retryenabled,
+ Double retrydelay,
+ Double timeout,
+ Integer tracelevel,
+ Integer mbusport,
+ String docprocChain) {
+
+ this.bindings = Collections.unmodifiableCollection(bindings);
+ this.abortondocumenterror = abortondocumenterror;
+ this.route = route;
+ this.maxpendingdocs = maxpendingdocs;
+ this.maxpendingbytes = maxpendingbytes;
+ this.retryenabled = retryenabled;
+ this.retrydelay = retrydelay;
+ this.timeout = timeout;
+ this.tracelevel = tracelevel;
+ this.mbusport = mbusport;
+ this.docprocChain = docprocChain;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/clients/HttpGatewayOwner.java b/config-model/src/main/java/com/yahoo/vespa/model/clients/HttpGatewayOwner.java
new file mode 100644
index 00000000000..b5118e6c6c7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/clients/HttpGatewayOwner.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients;
+
+import com.yahoo.config.subscription.ConfigInstanceUtil;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+public class HttpGatewayOwner extends AbstractConfigProducer implements FeederConfig.Producer {
+ private final FeederConfig.Builder feederConfig;
+
+ public HttpGatewayOwner(AbstractConfigProducer parent, FeederConfig.Builder feederConfig) {
+ super(parent, "gateways");
+ this.feederConfig = feederConfig;
+ }
+
+ @Override
+ public void getConfig(FeederConfig.Builder builder) {
+ ConfigInstanceUtil.setValues(builder, feederConfig);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolMaster.java b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolMaster.java
new file mode 100644
index 00000000000..1546b2d8433
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolMaster.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+
+/**
+ * The spoolmaster program, which is used when multiple spooler instances are used to provide
+ * multi colo HTTP feeding.
+ * @author vegardh
+ *
+ */
+public class VespaSpoolMaster extends AbstractService {
+
+ public VespaSpoolMaster(AbstractConfigProducer parent, int index) {
+ super(parent, "spoolmaster."+index);
+ }
+
+ @Override
+ public int getPortCount() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public String getStartupCommand() {
+ return "exec spoolmaster";
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpooler.java b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpooler.java
new file mode 100644
index 00000000000..7a4ec77b2da
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpooler.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients;
+
+import com.yahoo.vespa.config.content.spooler.SpoolerConfig;
+import com.yahoo.config.subscription.ConfigInstanceUtil;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+/**
+ * Holds configuration for VespaSpoolers. Actual services use VespaSpoolerService,
+ * while virtual services can be generated for external spoolers (VespaSpoolerProducer).
+ *
+ * @author <a href="mailto:thomasg@yahoo-inc.com">Gunnar Gauslaa Bergem</a>
+ * @author Vidar Larsen
+ */
+public class VespaSpooler {
+ private final SpoolerConfig.Builder spoolConfig;
+ private final FeederConfig.Builder feederConfig;
+
+ public VespaSpooler(FeederConfig.Builder feederConfig, SpoolerConfig.Builder spoolConfig) {
+ this.feederConfig = feederConfig;
+ this.spoolConfig = spoolConfig;
+ }
+
+ public void getConfig(SpoolerConfig.Builder builder) {
+ ConfigInstanceUtil.setValues(builder, spoolConfig);
+ }
+
+ public void getConfig(FeederConfig.Builder builder) {
+ ConfigInstanceUtil.setValues(builder, feederConfig);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerProducer.java
new file mode 100644
index 00000000000..671603553ec
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerProducer.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients;
+
+import com.yahoo.vespa.config.content.spooler.SpoolerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+/**
+ * This model represents a config producer for spooler used for feeding documents to Vespa.
+ *
+ * @author <a href="mailto:gunnarga@yahoo-inc.com">Gunnar Gauslaa Bergem</a>
+ * @author Vidar Larsen
+ */
+public class VespaSpoolerProducer extends AbstractConfigProducer implements SpoolerConfig.Producer, FeederConfig.Producer {
+ private static final long serialVersionUID = 1L;
+ private VespaSpooler spoolerConfig;
+
+ public VespaSpoolerProducer(AbstractConfigProducer parent, String configId, VespaSpooler spooler) {
+ super(parent, configId);
+ spoolerConfig = spooler;
+ }
+
+ @Override
+ public void getConfig(SpoolerConfig.Builder builder) {
+ spoolerConfig.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(FeederConfig.Builder builder) {
+ spoolerConfig.getConfig(builder);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerService.java b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerService.java
new file mode 100644
index 00000000000..8e62b797441
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/clients/VespaSpoolerService.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients;
+
+import com.yahoo.vespa.config.content.spooler.SpoolerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+/**
+ * This model represents a spooler used for feeding documents to Vespa.
+ *
+ * @author <a href="mailto:gunnarga@yahoo-inc.com">Gunnar Gauslaa Bergem</a>
+ * @author Vidar Larsen
+ */
+public class VespaSpoolerService extends AbstractService implements SpoolerConfig.Producer, FeederConfig.Producer {
+ private static final long serialVersionUID = 1L;
+ private VespaSpooler spooler;
+
+ public VespaSpoolerService(AbstractConfigProducer parent, int index, VespaSpooler spooler) {
+ super(parent, "spooler." + index);
+ this.spooler = spooler;
+ monitorService("spooler");
+ }
+
+ public int getPortCount() {
+ return 0;
+ }
+
+ public String getStartupCommand() {
+ return "exec vespaspooler "+getJvmArgs();
+ }
+
+ @Override
+ public void getConfig(SpoolerConfig.Builder builder) {
+ spooler.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(FeederConfig.Builder builder) {
+ spooler.getConfig(builder);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java
new file mode 100644
index 00000000000..a13c7c9cec4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java
@@ -0,0 +1,408 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.container.QrConfig;
+import com.yahoo.container.core.ContainerHttpConfig;
+import com.yahoo.container.jdisc.ContainerMbusConfig;
+import com.yahoo.container.jdisc.JdiscBindingsConfig;
+import com.yahoo.container.jdisc.config.PortOverridesConfig;
+import com.yahoo.search.config.QrStartConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.ComponentGroup;
+import com.yahoo.vespa.model.container.component.ComponentsConfigGenerator;
+import com.yahoo.vespa.model.container.component.DiscBindingsConfigGenerator;
+import com.yahoo.vespa.model.container.component.Handler;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+import com.yahoo.vespa.model.container.http.ConnectorFactory;
+import com.yahoo.vespa.model.container.http.Http;
+import com.yahoo.vespa.model.container.http.JettyHttpServer;
+import com.yahoo.vespa.model.filedistribution.FileDistributionConfigProducer;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import static com.yahoo.container.QrConfig.Filedistributor;
+import static com.yahoo.container.QrConfig.Rpc;
+
+
+/**
+ * @author gjoranv
+ * @author einarmr
+ * @author tonytv
+ */
+//qr is restart because it is handled by ConfiguredApplication.start
+@RestartConfigs({QrStartConfig.class, QrConfig.class})
+public class Container extends AbstractService implements
+ QrConfig.Producer,
+ ComponentsConfig.Producer,
+ JdiscBindingsConfig.Producer,
+ ContainerHttpConfig.Producer,
+ PortOverridesConfig.Producer,
+ ContainerMbusConfig.Producer
+{
+ public static final class PortOverride {
+ public final ComponentSpecification serverId;
+ public final int port;
+
+ public PortOverride(ComponentSpecification serverId, int port) {
+ this.serverId = serverId;
+ this.port = port;
+ }
+ }
+
+ public static final int BASEPORT = Defaults.getDefaults().vespaWebServicePort();
+ public static final String SINGLENODE_CONTAINER_SERVICESPEC = "default_singlenode_container";
+
+ private final AbstractConfigProducer parent;
+ private final String name;
+ private String clusterName = null;
+ private boolean rpcServerEnabled = true;
+
+ // TODO: move these up to cluster
+ private boolean httpServerEnabled = true;
+ private boolean messageBusEnabled = true;
+
+ private final boolean retired;
+
+ private final ComponentGroup<Handler<?>> handlers = new ComponentGroup<>(this, "handler");
+ private final ComponentGroup<Component<?, ?>> components = new ComponentGroup(this, "components");
+
+ private final JettyHttpServer defaultHttpServer = new JettyHttpServer(new ComponentId("DefaultHttpServer"));
+
+ private final List<PortOverride> portOverrides;
+
+ private final int numHttpServerPorts;
+ private final int numRpcServerPorts = 2;
+ private static String defaultHostedJVMArgs = "-XX:+UseOSErrorReporting -XX:+SuppressFatalErrorMessage";
+
+ public Container(AbstractConfigProducer parent, String name) {
+ this(parent, name, Collections.<PortOverride>emptyList());
+ }
+ public Container(AbstractConfigProducer parent, String name, boolean retired) {
+ this(parent, name, retired, Collections.<PortOverride>emptyList());
+ }
+ public Container(AbstractConfigProducer parent, String name, List<PortOverride> portOverrides) {
+ this(parent, name, false, portOverrides);
+ }
+ public Container(AbstractConfigProducer parent, String name, boolean retired, List<PortOverride> portOverrides) {
+ super(parent, name);
+ this.name = name;
+ this.parent = parent;
+ this.portOverrides = Collections.unmodifiableList(new ArrayList<>(portOverrides));
+ this.retired = retired;
+
+ if (getHttp() == null) {
+ numHttpServerPorts = 2;
+ addChild(defaultHttpServer);
+ } else if (getHttp().getHttpServer() == null) {
+ numHttpServerPorts = 0;
+ } else {
+ numHttpServerPorts = getHttp().getHttpServer().getConnectorFactories().size();
+ }
+ addBuiltinHandlers();
+
+ addChild(new SimpleComponent("com.yahoo.container.jdisc.ConfiguredApplication$ApplicationContext"));
+ addChild(new SimpleComponent("com.yahoo.container.jdisc.ContainerPortsOverride"));
+ }
+
+ /** True if this container is retired (slated for removal) */
+ public boolean isRetired() { return retired; }
+
+ public ComponentGroup<Handler<?>> getHandlers() {
+ return handlers;
+ }
+
+ public ComponentGroup getComponents() {
+ return components;
+ }
+
+ public void addComponent(Component c) {
+ components.addComponent(c);
+ }
+
+ public void addHandler(Handler h) {
+ handlers.addComponent(h);
+ }
+
+ public Http getHttp() {
+ return (parent instanceof ContainerCluster) ?
+ ((ContainerCluster) parent).getHttp():
+ null;
+ }
+
+ public JettyHttpServer getDefaultHttpServer() {
+ return defaultHttpServer;
+ }
+
+ // We cannot set bindings yet, as baseport is not initialized
+ public void addBuiltinHandlers() {
+ }
+
+ @Override
+ public void initService() {
+ // XXX: Must be called first, to set the baseport
+ super.initService();
+
+ if (getHttp() == null) {
+ initDefaultJettyConnector();
+ } else {
+ reserveHttpPortsPrepended();
+ }
+
+ tagServers();
+ monitorService();
+ }
+
+ private void tagServers() {
+ if (numHttpServerPorts > 0) {
+ portsMeta.on(0).tag("http").tag("query").tag("external").tag("state");
+ }
+
+ for (int i = 1; i < numHttpServerPorts; i++)
+ portsMeta.on(i).tag("http").tag("external");
+
+ if (rpcServerEnabled) {
+ portsMeta.on(numHttpServerPorts + 0).tag("rpc").tag("messaging");
+ portsMeta.on(numHttpServerPorts + 1).tag("rpc").tag("admin");
+ }
+ }
+
+ private void reserveHttpPortsPrepended() {
+ if (getHttp().getHttpServer() != null) {
+ for (ConnectorFactory connectorFactory : getHttp().getHttpServer().getConnectorFactories()) {
+ reservePortPrepended(getPort(connectorFactory, portOverrides));
+ }
+ }
+ }
+
+ private int getPort(ConnectorFactory connectorFactory, List<PortOverride> portOverrides) {
+ ComponentId id = ComponentId.fromString(connectorFactory.getName());
+ for (PortOverride override : portOverrides) {
+ if (override.serverId.matches(id)) {
+ return override.port;
+ }
+ }
+ return connectorFactory.getListenPort();
+ }
+
+ private void initDefaultJettyConnector() {
+ defaultHttpServer.addConnector(new ConnectorFactory("SearchServer", getSearchPort(), null));
+ }
+
+ private boolean hasDocproc() {
+ return (parent instanceof ContainerCluster) && (((ContainerCluster)parent).getDocproc() != null);
+ }
+
+ // TODO: hack to retain old service names, e.g. in ymon config, vespa.log etc.
+ @Override
+ public String getServiceType() {
+ if (parent instanceof ContainerCluster) {
+ ContainerCluster cluster = (ContainerCluster)parent;
+ if (cluster.getSearch() != null && cluster.getDocproc() == null && cluster.getDocumentApi() == null) {
+ return "qrserver";
+ }
+ if (cluster.getSearch() == null && cluster.getDocproc() != null) {
+ return "docprocservice";
+ }
+ }
+ return super.getServiceType();
+ }
+
+ public void setClusterName(String name) {
+ this.clusterName = name;
+ }
+
+ @Override
+ public int getWantedPort() {
+ return getHttp() == null ?
+ BASEPORT:
+ 0;
+ }
+
+ /**
+ * First Qrserver or container must run on ports familiar to the user.
+ */
+ @Override
+ public boolean requiresWantedPort() {
+ return getHttp() == null;
+
+ }
+
+ public boolean requiresConsecutivePorts() {
+ return false;
+ }
+
+ /**
+ * @return the number of ports needed by the Container - those reserved manually(reservePortPrepended)
+ */
+ public int getPortCount() {
+ int httpPorts = (getHttp() != null) ? 0 : numHttpServerPorts + 2; // TODO remove +2, only here to keep irrelevant unit tests from failing.
+ int rpcPorts = (isRpcServerEnabled()) ? numRpcServerPorts : 0;
+ return httpPorts + rpcPorts;
+ }
+
+ /**
+ * @return the actual search port
+ * TODO: Remove. Use {@link #getPortsMeta()} and check tags in conjunction with {@link #getRelativePort(int)}.
+ */
+ public int getSearchPort(){
+ if (getHttp() != null)
+ throw new AssertionError("getSearchPort must not be used when http section is present.");
+
+ return getRelativePort(0);
+ }
+
+ private int getRpcPort() {
+ return isRpcServerEnabled() ?
+ getRelativePort(numHttpServerPorts + 1) :
+ 0;
+ }
+
+ private int getMessagingPort() {
+ return getRelativePort(numHttpServerPorts);
+ }
+
+ @Override
+ public int getHealthPort() {
+ final Http http = getHttp();
+ if (http != null) {
+ // TODO: allow the user to specify health port manually
+ if (http.getHttpServer() == null) {
+ return -1;
+ } else {
+ return getRelativePort(0);
+ }
+ } else {
+ return httpServerEnabled ? getSearchPort() : -1;
+ }
+ }
+
+ public String getStartupCommand() {
+ return "PRELOAD=" + getPreLoad() + " exec vespa-start-container-daemon " + getJvmArgs() + " ";
+ }
+
+ public boolean isRpcServerEnabled() {
+ return rpcServerEnabled;
+ }
+
+ @Override
+ public void getConfig(PortOverridesConfig.Builder builder) {
+ for (PortOverride portOverride: portOverrides) {
+ builder.server(new PortOverridesConfig.Server.Builder().
+ id(portOverride.serverId.stringValue()).
+ port(portOverride.port));
+ }
+ }
+
+ @Override
+ public void getConfig(QrConfig.Builder builder) {
+ builder.
+ rpc(new Rpc.Builder()
+ .enabled(isRpcServerEnabled())
+ .port(getRpcPort())
+ .slobrokId(serviceSlobrokId())).
+ filedistributor(filedistributorConfig());
+ if (clusterName != null) {
+ builder.discriminator(clusterName+"."+name);
+ } else {
+ builder.discriminator(name);
+ }
+ }
+
+ @Override
+ public String getJvmArgs() {
+ String jvmArgs = super.getJvmArgs();
+ return isHostedVespa() && hasDocproc()
+ ? ("".equals(jvmArgs) ? defaultHostedJVMArgs : defaultHostedJVMArgs + " " + jvmArgs)
+ : jvmArgs;
+ }
+ private String serviceSlobrokId() {
+ return "vespa/service/" + getConfigId();
+ }
+
+ private Filedistributor.Builder filedistributorConfig() {
+ Filedistributor.Builder builder = new Filedistributor.Builder();
+
+ FileDistributionConfigProducer fileDistribution = getRoot().getFileDistributionConfigProducer();
+ if (fileDistribution != null)
+ builder.configid(fileDistribution.getFileDistributionServiceConfigId(getHost()));
+
+ return builder;
+ }
+
+ @Override
+ public void getConfig(ComponentsConfig.Builder builder) {
+ builder.components.addAll(
+ ComponentsConfigGenerator.generate(allEnabledComponents()));
+ }
+
+ private Collection<Component<?, ?>> allEnabledComponents() {
+ Collection<Component<?, ?>> allComponents = new ArrayList<>();
+ addAllEnabledComponents(allComponents, this);
+ return Collections.unmodifiableCollection(allComponents);
+ }
+
+ private void addAllEnabledComponents(Collection<Component<?, ?>> allComponents, AbstractConfigProducer<?> current) {
+ for (AbstractConfigProducer<?> child: current.getChildren().values()) {
+ if (!httpServerEnabled && isHttpServer(child))
+ continue;
+
+ if (child instanceof Component) {
+ allComponents.add((Component<?, ?>) child);
+ }
+
+ addAllEnabledComponents(allComponents, child);
+ }
+ }
+
+ private boolean isHttpServer(AbstractConfigProducer<?> component) {
+ return component instanceof JettyHttpServer;
+ }
+
+ @Override
+ public final void getConfig(JdiscBindingsConfig.Builder builder) {
+ builder.handlers(DiscBindingsConfigGenerator.generate(handlers.getComponents()));
+ }
+
+ @Override
+ public void getConfig(ContainerHttpConfig.Builder builder) {
+ builder
+ .enabled(httpServerEnabled)
+ .port(new ContainerHttpConfig.Port.Builder()
+ .search(getSearchPort()));
+ }
+
+ @Override
+ public void getConfig(ContainerMbusConfig.Builder builder) {
+ builder.enabled(messageBusEnabled).
+ port(getMessagingPort());
+ }
+
+ @Override
+ public HashMap<String,String> getDefaultMetricDimensions(){
+ HashMap<String, String> dimensions = new HashMap<>();
+ if (clusterName != null) {
+ dimensions.put("clustername", clusterName);
+ }
+ return dimensions;
+ }
+
+ public void setRpcServerEnabled(boolean rpcServerEnabled) {
+ this.rpcServerEnabled = rpcServerEnabled;
+ }
+
+ public void setHttpServerEnabled(boolean httpServerEnabled) {
+ this.httpServerEnabled = httpServerEnabled;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java
new file mode 100755
index 00000000000..ae158567587
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java
@@ -0,0 +1,816 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container;
+
+import com.yahoo.cloud.config.ClusterInfoConfig;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.cloud.config.RoutingProviderConfig;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.FileReference;
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.docproc.DocprocConfig;
+import com.yahoo.config.docproc.SchemamappingConfig;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ComponentInfo;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.BundlesConfig;
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.container.QrSearchersConfig;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.core.ApplicationMetadataConfig;
+import com.yahoo.container.core.document.ContainerDocumentConfig;
+import com.yahoo.container.handler.ThreadPoolProvider;
+import com.yahoo.container.jdisc.ContainerMbusConfig;
+import com.yahoo.container.jdisc.JdiscBindingsConfig;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.container.jdisc.config.MetricDefaultsConfig;
+import com.yahoo.container.jdisc.messagebus.MbusServerProvider;
+import com.yahoo.container.jdisc.state.StateHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.container.usability.BindingsOverviewHandler;
+import com.yahoo.container.xml.providers.DatatypeFactoryProvider;
+import com.yahoo.container.xml.providers.DocumentBuilderFactoryProvider;
+import com.yahoo.container.xml.providers.JAXBContextFactoryProvider;
+import com.yahoo.container.xml.providers.SAXParserFactoryProvider;
+import com.yahoo.container.xml.providers.SchemaFactoryProvider;
+import com.yahoo.container.xml.providers.TransformerFactoryProvider;
+import com.yahoo.container.xml.providers.XMLEventFactoryProvider;
+import com.yahoo.container.xml.providers.XMLInputFactoryProvider;
+import com.yahoo.container.xml.providers.XMLOutputFactoryProvider;
+import com.yahoo.container.xml.providers.XPathFactoryProvider;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.jdisc.http.ServletPathsConfig;
+import com.yahoo.metrics.simple.runtime.MetricProperties;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.prelude.semantics.SemanticRulesConfig;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.search.config.QrStartConfig;
+import com.yahoo.search.pagetemplates.PageTemplatesConfig;
+import com.yahoo.search.query.profile.config.QueryProfilesConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import com.yahoo.vespa.model.PortsMeta;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.admin.MonitoringSystem;
+import com.yahoo.vespa.model.clients.ContainerDocumentApi;
+import com.yahoo.vespa.model.container.component.AccessLogComponent;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.ComponentGroup;
+import com.yahoo.vespa.model.container.component.ComponentsConfigGenerator;
+import com.yahoo.vespa.model.container.component.ConfigProducerGroup;
+import com.yahoo.vespa.model.container.component.DiscBindingsConfigGenerator;
+import com.yahoo.vespa.model.container.component.Handler;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+import com.yahoo.vespa.model.container.component.Servlet;
+import com.yahoo.vespa.model.container.component.StatisticsComponent;
+import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.container.docproc.DocprocChains;
+import com.yahoo.vespa.model.container.http.Http;
+import com.yahoo.vespa.model.container.jersey.Jersey2Servlet;
+import com.yahoo.vespa.model.container.jersey.JerseyHandler;
+import com.yahoo.vespa.model.container.jersey.RestApi;
+import com.yahoo.vespa.model.container.processing.ProcessingChains;
+import com.yahoo.vespa.model.container.search.ContainerSearch;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.utils.FileSender;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import java.io.Reader;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static com.yahoo.container.core.BundleLoaderProperties.DISK_BUNDLE_PREFIX;
+
+/**
+ * @author gjoranv
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @author tonytv
+ */
+public final class ContainerCluster
+ extends AbstractConfigProducer<AbstractConfigProducer<?>>
+ implements
+ ComponentsConfig.Producer,
+ JdiscBindingsConfig.Producer,
+ DocumentmanagerConfig.Producer,
+ ContainerMbusConfig.Producer,
+ ContainerDocumentConfig.Producer,
+ HealthMonitorConfig.Producer,
+ ApplicationMetadataConfig.Producer,
+ BundlesConfig.Producer,
+ FeederConfig.Producer,
+ IndexInfoConfig.Producer,
+ IlscriptsConfig.Producer,
+ SchemamappingConfig.Producer,
+ QrSearchersConfig.Producer,
+ QrStartConfig.Producer,
+ QueryProfilesConfig.Producer,
+ PageTemplatesConfig.Producer,
+ SemanticRulesConfig.Producer,
+ DocprocConfig.Producer,
+ MetricDefaultsConfig.Producer,
+ ClusterInfoConfig.Producer,
+ ServletPathsConfig.Producer,
+ RoutingProviderConfig.Producer,
+ ConfigserverConfig.Producer {
+ /**
+ * URI prefix used for internal, usually programmatic, APIs. URIs using this
+ * prefix should never considered available for direct use by customers, and
+ * normal compatibility concerns only applies to libraries using the URIs in
+ * question, not contents served from the URIs themselves.
+ */
+ public static final String RESERVED_URI_PREFIX = "reserved-for-internal-use";
+
+ public static final String ROOT_HANDLER_BINDING = "*://*/";
+
+ private String name;
+
+ private List<Container> containers = new ArrayList<>();
+
+ private Http http;
+ private ProcessingChains processingChains;
+ private ContainerSearch containerSearch;
+ private ContainerDocproc containerDocproc;
+ private ContainerDocumentApi containerDocumentApi;
+
+ private MbusParams mbusParams;
+
+ private final Set<FileReference> applicationBundles = new LinkedHashSet<>();
+ private final Set<Path> platformBundles = new LinkedHashSet<>();
+
+ private final List<String> serviceAliases = new ArrayList<>();
+ private final List<String> endpointAliases = new ArrayList<>();
+ protected final ComponentGroup<Component<?, ?>> componentGroup;
+ private final ConfigProducerGroup<RestApi> restApiGroup;
+ private final ConfigProducerGroup<Servlet> servletGroup;
+
+ private Map<String, String> concreteDocumentTypes = new LinkedHashMap<>();
+ private MetricDefaultsConfig.Factory.Enum defaultMetricConsumerFactory;
+
+ private ApplicationMetaData applicationMetaData = null;
+
+ /** The zone this is deployed in, or the default zone if not on hosted Vespa */
+ private Zone zone;
+
+ public ContainerCluster(AbstractConfigProducer<?> parent, String subId, String name) {
+ super(parent, subId);
+ this.name = name;
+ this.zone = getRoot() != null ? getRoot().getDeployState().zone() : Zone.defaultZone();
+ componentGroup = new ComponentGroup<>(this, "component");
+ restApiGroup = new ConfigProducerGroup<>(this, "rest-api");
+ servletGroup = new ConfigProducerGroup<>(this, "servlet");
+
+ addComponent(new StatisticsComponent());
+ addSimpleComponent(AccessLog.class);
+ // TODO better modelling
+ addSimpleComponent(ThreadPoolProvider.class);
+ addSimpleComponent("com.yahoo.jdisc.http.filter.SecurityFilterInvoker");
+ addSimpleComponent("com.yahoo.language.provider.SimpleLinguisticsProvider");
+ addSimpleComponent("com.yahoo.container.jdisc.SslKeyStoreFactoryProvider");
+ addSimpleComponent("com.yahoo.container.jdisc.SecretStoreProvider");
+ addSimpleComponent("com.yahoo.container.jdisc.CertificateStoreProvider");
+ addSimpleComponent("com.yahoo.container.jdisc.metric.MetricConsumerProviderProvider");
+ addSimpleComponent("com.yahoo.container.jdisc.metric.MetricProvider");
+ addSimpleComponent("com.yahoo.container.jdisc.metric.MetricUpdater");
+ addSimpleComponent(com.yahoo.metrics.simple.MetricManager.class.getName(), null, MetricProperties.BUNDLE_SYMBOLIC_NAME);
+ addSimpleComponent(com.yahoo.metrics.simple.jdisc.JdiscMetricsFactory.class.getName(), null, MetricProperties.BUNDLE_SYMBOLIC_NAME);
+ addSimpleComponent("com.yahoo.container.jdisc.state.StateMonitor");
+ addSimpleComponent("com.yahoo.container.jdisc.ContainerThreadFactory");
+ addSimpleComponent("com.yahoo.container.protect.FreezeDetector");
+ addSimpleComponent("com.yahoo.container.core.slobrok.SlobrokConfigurator");
+ addSimpleComponent("com.yahoo.container.handler.VipStatus");
+ addJaxProviders();
+ }
+
+ public void setZone(Zone zone) {
+ this.zone = zone;
+ }
+
+ public void addMetricStateHandler() {
+ Handler<AbstractConfigProducer<?>> stateHandler = new Handler<>(
+ new ComponentModel("com.yahoo.container.jdisc.state.StateHandler", null, null, null));
+ stateHandler.addServerBindings("http://*" + StateHandler.STATE_API_ROOT,
+ "https://*" + StateHandler.STATE_API_ROOT,
+ "http://*" + StateHandler.STATE_API_ROOT + "/*",
+ "https://*" + StateHandler.STATE_API_ROOT + "/*");
+ addComponent(stateHandler);
+ }
+
+ public void addDefaultRootHandler() {
+ if (hasHandlerWithBinding(ROOT_HANDLER_BINDING))
+ return;
+
+ Handler<AbstractConfigProducer<?>> handler = new Handler<>(
+ new ComponentModel(BundleInstantiationSpecification.getFromStrings(
+ BindingsOverviewHandler.class.getName(), null, null), null)); // null bundle, as the handler is in container-disc
+ handler.addServerBindings(ROOT_HANDLER_BINDING);
+ addComponent(handler);
+ }
+
+ private boolean hasHandlerWithBinding(String binding) {
+ Collection<Handler<?>> handlers = getHandlers();
+ for (Handler handler : handlers) {
+ if (handler.getServerBindings().contains(binding))
+ return true;
+ }
+ return false;
+ }
+
+ public void addApplicationStatusHandler() {
+ Handler<AbstractConfigProducer<?>> statusHandler = new Handler<>(
+ new ComponentModel(BundleInstantiationSpecification.getInternalHandlerSpecificationFromStrings(
+ "com.yahoo.container.handler.observability.ApplicationStatusHandler", null), null));
+ statusHandler.addServerBindings("http://*/ApplicationStatus",
+ "https://*/ApplicationStatus");
+ addComponent(statusHandler);
+ }
+
+ public void addVipHandler() {
+ Handler<?> vipHandler = Handler.fromClassName("com.yahoo.container.handler.VipStatusHandler");
+ vipHandler.addServerBindings("http://*/status.html", "https://*/status.html");
+ addComponent(vipHandler);
+ }
+
+ public void addStatisticsHandler() {
+ Handler<?> statsHandler = Handler.fromClassName("com.yahoo.container.config.StatisticsRequestHandler");
+ statsHandler.addServerBindings("http://*/statistics/*", "https://*/statistics/*");
+ addComponent(statsHandler);
+ }
+
+ public void addJaxProviders() {
+ addSimpleComponent(DatatypeFactoryProvider.class);
+ addSimpleComponent(DocumentBuilderFactoryProvider.class);
+ addSimpleComponent(JAXBContextFactoryProvider.class);
+ addSimpleComponent(SAXParserFactoryProvider.class);
+ addSimpleComponent(SchemaFactoryProvider.class);
+ addSimpleComponent(TransformerFactoryProvider.class);
+ addSimpleComponent(XMLEventFactoryProvider.class);
+ addSimpleComponent(XMLInputFactoryProvider.class);
+ addSimpleComponent(XMLOutputFactoryProvider.class);
+ addSimpleComponent(XPathFactoryProvider.class);
+ }
+
+ public final void addComponent(Component<?, ?> component) {
+ componentGroup.addComponent(component);
+ }
+
+ public final void addComponents(Collection<Component<?, ?>> components) {
+ for (Component<?, ?> component : components) {
+ addComponent(component);
+ }
+ }
+
+ public final void addSimpleComponent(String idSpec, String classSpec, String bundleSpec) {
+ addComponent(new SimpleComponent(new ComponentModel(idSpec, classSpec, bundleSpec)));
+ }
+
+ /**
+ * Removes a component by id
+ *
+ * @return the removed component, or null if it was not present
+ */
+ public Component removeComponent(ComponentId componentId) {
+ return componentGroup.removeComponent(componentId);
+ }
+
+ public void addSimpleComponent(Class<?> clazz) {
+ addSimpleComponent(clazz.getName());
+ }
+
+ private void addSimpleComponent(String className) {
+ addComponent(new SimpleComponent(className));
+ }
+
+ public void prepare() {
+ addAndSendApplicationBundles();
+ sendUserConfiguredFiles();
+ setApplicationMetaData();
+ for (RestApi restApi : restApiGroup.getComponents())
+ restApi.prepare();
+ }
+
+ private void setApplicationMetaData() {
+ applicationMetaData = getRoot().getDeployState().getApplicationPackage().getMetaData();
+ }
+
+ public void addMbusServer(ComponentId chainId) {
+ ComponentId serviceId = chainId.nestInNamespace(ComponentId.fromString("MbusServer"));
+
+ addComponent(
+ new Component<>(new ComponentModel(new BundleInstantiationSpecification(
+ serviceId,
+ ComponentSpecification.fromString(MbusServerProvider.class.getName()),
+ null))));
+ }
+
+ private void addAndSendApplicationBundles() {
+ for (ComponentInfo component : getRoot().getDeployState().getApplicationPackage().getComponentsInfo(getRoot().getDeployState().getProperties().vespaVersion())) {
+ FileReference reference = FileSender.sendFileToServices(component.getPathRelativeToAppDir(), containers);
+ applicationBundles.add(reference);
+ }
+ }
+
+ private void sendUserConfiguredFiles() {
+ // Files referenced from user configs to all components.
+ for (Component<?, ?> component : getAllComponents()) {
+ FileSender.sendUserConfiguredFiles(component, containers, deployLogger());
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<Container> getContainers() {
+ return Collections.unmodifiableList(containers);
+ }
+
+ public void addContainers(Collection<Container> containers) {
+ int index = this.containers.size();
+ for (Container container : containers) {
+ container.setClusterName(name);
+ container.setProp("clustername", name)
+ .setProp("index", index++);
+ setRotations(container, getRotations(), getGlobalServiceId(), name);
+ container.setProp("activeRotation", Boolean.toString(getActiveRotation()));
+ }
+ this.containers.addAll(containers);
+ }
+
+ private Optional<String> getGlobalServiceId() {
+ Optional<String> globalServiceId = Optional.empty();
+ Optional<DeploymentSpec> deploymentSpec = getDeploymentSpec();
+ if (deploymentSpec.isPresent()) {
+ globalServiceId = deploymentSpec.get().globalServiceId();
+ }
+ return globalServiceId;
+ }
+
+ private Set<Rotation> getRotations() {
+ return Optional.ofNullable(getRoot())
+ .map(root -> root.getDeployState().getRotations())
+ .orElse(Collections.emptySet());
+ }
+
+ private boolean getActiveRotation() {
+ return Optional.ofNullable(getRoot())
+ .map(root -> root.getDeployState().getProperties().zone())
+ .map(this::zoneHasActiveRotation)
+ .orElse(false);
+ }
+
+ private boolean zoneHasActiveRotation(Zone zone) {
+ return getDeploymentSpec()
+ .flatMap(spec -> spec.zones().stream()
+ .filter(dz -> dz.matches(zone.environment(), Optional.of(zone.region())))
+ .findFirst())
+ .map(DeploymentSpec.DeclaredZone::active)
+ .orElse(false);
+ }
+
+ private Optional<DeploymentSpec> getDeploymentSpec() {
+ Optional<DeploymentSpec> deploymentSpec = Optional.empty();
+ AbstractConfigProducerRoot root = getRoot();
+ if (root != null) {
+ final Optional<Reader> deployment = root.getDeployState().getApplicationPackage().getDeployment();
+ if (deployment.isPresent()) {
+ deploymentSpec = Optional.of(DeploymentSpec.fromXml(deployment.get()));
+ }
+ }
+ return deploymentSpec;
+ }
+
+ private void setRotations(Container container, Set<Rotation> rotations, Optional<String> globalServiceId, String containerClusterName) {
+ if (!rotations.isEmpty() && globalServiceId.isPresent()) {
+ if (containerClusterName.equals(globalServiceId.get())) {
+ container.setProp("rotations", rotations.stream().map(Rotation::getId).collect(Collectors.joining(",")));
+ }
+ }
+ }
+
+ public void addContainer(Container container) {
+ addContainers(Collections.singletonList(container));
+ }
+
+ public void setProcessingChains(ProcessingChains processingChains, String... serverBindings) {
+ if (this.processingChains != null)
+ throw new IllegalStateException("ProcessingChains should only be set once.");
+
+ this.processingChains = processingChains;
+
+ // Cannot use the class object for ProcessingHandler, because its superclass is not accessible
+ ProcessingHandler<?> processingHandler = new ProcessingHandler<>(
+ processingChains,
+ "com.yahoo.processing.handler.ProcessingHandler");
+
+ for (String binding: serverBindings)
+ processingHandler.addServerBindings(binding);
+
+ addComponent(processingHandler);
+ }
+
+ public ProcessingChains getProcessingChains() {
+ return processingChains;
+ }
+
+ @NonNull
+ public SearchChains getSearchChains() {
+ if (containerSearch == null)
+ throw new IllegalStateException("Null search components!");
+ return containerSearch.getChains();
+ }
+
+ @Nullable
+ public ContainerSearch getSearch() {
+ return containerSearch;
+ }
+
+ public void setSearch(ContainerSearch containerSearch) {
+ this.containerSearch = containerSearch;
+ }
+
+ public void setHttp(Http http) {
+ this.http = http;
+ addChild(http);
+ }
+
+ @Nullable
+ public Http getHttp() {
+ return http;
+ }
+
+ public final void addRestApi(@NonNull RestApi restApi) {
+ restApiGroup.addComponent(ComponentId.fromString(restApi.getBindingPath()), restApi);
+ }
+
+ public Map<ComponentId, RestApi> getRestApiMap() {
+ return restApiGroup.getComponentMap();
+ }
+
+ public Map<ComponentId, Servlet> getServletMap() {
+ return servletGroup.getComponentMap();
+ }
+
+ public final void addServlet(@NonNull Servlet servlet) {
+ servletGroup.addComponent(servlet.getGlobalComponentId(), servlet);
+ }
+
+ @Nullable
+ public ContainerDocproc getDocproc() {
+ return containerDocproc;
+ }
+
+ public void setDocproc(ContainerDocproc containerDocproc) {
+ this.containerDocproc = containerDocproc;
+ }
+
+ @Nullable
+ public ContainerDocumentApi getDocumentApi() {
+ return containerDocumentApi;
+ }
+
+ public void setDocumentApi(ContainerDocumentApi containerDocumentApi) {
+ this.containerDocumentApi = containerDocumentApi;
+ }
+
+ @NonNull
+ public DocprocChains getDocprocChains() {
+ if (containerDocproc == null)
+ throw new IllegalStateException("Null docproc components!");
+ return containerDocproc.getChains();
+ }
+
+ @SuppressWarnings("unchecked")
+ public Collection<Handler<?>> getHandlers() {
+ return (Collection<Handler<?>>)(Collection)componentGroup.getComponents(Handler.class);
+ }
+
+ public Map<ComponentId, Component<?, ?>> getComponentsMap() {
+ return componentGroup.getComponentMap();
+ }
+
+ /** Returns all components in this cluster (generic, handlers, chained) */
+ public Collection<Component<?, ?>> getAllComponents() {
+ List<Component<?, ?>> allComponents = new ArrayList<>();
+ recursivelyFindAllComponents(allComponents, this);
+ // We need consistent ordering
+ Collections.sort(allComponents);
+ return Collections.unmodifiableCollection(allComponents);
+ }
+
+ private void recursivelyFindAllComponents(Collection<Component<?, ?>> allComponents, AbstractConfigProducer<?> current) {
+ for (AbstractConfigProducer<?> child: current.getChildren().values()) {
+ if (child instanceof Component)
+ allComponents.add((Component<?, ?>) child);
+
+ if (!(child instanceof Container))
+ recursivelyFindAllComponents(allComponents, child);
+ }
+ }
+
+ @Override
+ public final void getConfig(ComponentsConfig.Builder builder) {
+ builder.components.addAll(ComponentsConfigGenerator.generate(getAllComponents()));
+ builder.components(new ComponentsConfig.Components.Builder().id("com.yahoo.container.core.config.HandlersConfigurerDi$RegistriesHack"));
+ }
+
+ @Override
+ public final void getConfig(JdiscBindingsConfig.Builder builder) {
+ builder.handlers.putAll(DiscBindingsConfigGenerator.generate(getHandlers()));
+
+ allJersey1Handlers().forEach(handler ->
+ builder.handlers.putAll(DiscBindingsConfigGenerator.generate(handler))
+ );
+ }
+
+ private Stream<JerseyHandler> allJersey1Handlers() {
+ return restApiGroup.getComponents().stream().flatMap(streamOf(RestApi::getJersey1Handler));
+ }
+
+ @Override
+ public void getConfig(ServletPathsConfig.Builder builder) {
+ allServlets().forEach(servlet ->
+ builder.servlets(servlet.getComponentId().stringValue(),
+ servlet.toConfigBuilder())
+ );
+ }
+
+ private Stream<Servlet> allServlets() {
+ return Stream.concat(allJersey2Servlets(),
+ servletGroup.getComponents().stream());
+ }
+
+ private Stream<Jersey2Servlet> allJersey2Servlets() {
+ return restApiGroup.getComponents().stream().flatMap(streamOf(RestApi::getJersey2Servlet));
+ }
+
+ private <T, R> Function<T, Stream<R>> streamOf(Function<T, Optional<R>> f) {
+ return t ->
+ f.apply(t).
+ <Stream<R>>map(Stream::of).
+ orElse(Stream.empty());
+ }
+
+ @Override
+ public void getConfig(DocumentmanagerConfig.Builder builder) {
+ if (containerDocproc != null && containerDocproc.isCompressDocuments())
+ builder.enablecompression(true);
+ }
+
+ @Override
+ public void getConfig(ContainerDocumentConfig.Builder builder) {
+ for (Map.Entry<String, String> e : concreteDocumentTypes.entrySet()) {
+ ContainerDocumentConfig.Doctype.Builder dtb = new ContainerDocumentConfig.Doctype.Builder();
+ dtb.type(e.getKey());
+ dtb.factorycomponent(e.getValue());
+ builder.doctype(dtb);
+ }
+ }
+
+ @Override
+ public void getConfig(HealthMonitorConfig.Builder builder) {
+ MonitoringSystem monitoringSystem = getMonitoringService();
+ if (monitoringSystem != null) {
+ builder.snapshot_interval(monitoringSystem.getIntervalSeconds());
+ }
+ }
+
+ @Override
+ public void getConfig(ApplicationMetadataConfig.Builder builder) {
+ if (applicationMetaData != null) {
+ builder.name(applicationMetaData.getApplicationName()).
+ user(applicationMetaData.getDeployedByUser()).
+ path(applicationMetaData.getDeployPath()).
+ timestamp(applicationMetaData.getDeployTimestamp()).
+ checksum(applicationMetaData.getCheckSum()).
+ generation(applicationMetaData.getGeneration());
+ }
+ }
+
+ /**
+ * Adds a bundle present at a known location at the target container nodes.
+ *
+ * @param bundlePath usually an absolute path, e.g. '$VESPA_HOME/lib/jars/foo.jar'
+ */
+ public final void addPlatformBundle(Path bundlePath) {
+ platformBundles.add(bundlePath);
+ }
+
+ @Override
+ public void getConfig(BundlesConfig.Builder builder) {
+ Stream.concat(applicationBundles.stream().map(FileReference::value),
+ platformBundles.stream().map(ContainerCluster::toFileReferenceString))
+ .forEach(builder::bundle);
+ }
+
+ private static String toFileReferenceString(Path path) {
+ return DISK_BUNDLE_PREFIX + path.toString();
+ }
+
+ @Override
+ public void getConfig(QrSearchersConfig.Builder builder) {
+ if (containerSearch!=null) containerSearch.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(QrStartConfig.Builder builder) {
+ if (containerSearch!=null) containerSearch.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(DocprocConfig.Builder builder) {
+ if (containerDocproc != null) {
+ containerDocproc.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(PageTemplatesConfig.Builder builder) {
+ if (containerSearch != null) {
+ containerSearch.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(SemanticRulesConfig.Builder builder) {
+ if (containerSearch != null) {
+ containerSearch.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(QueryProfilesConfig.Builder builder) {
+ if (containerSearch != null) {
+ containerSearch.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(SchemamappingConfig.Builder builder) {
+ if (containerDocproc!=null) containerDocproc.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ if (containerSearch!=null) containerSearch.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(FeederConfig.Builder builder) {
+ if (containerDocumentApi != null) {
+ containerDocumentApi.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(ContainerMbusConfig.Builder builder) {
+ if (mbusParams != null) {
+ if (mbusParams.maxConcurrentFactor != null)
+ builder.maxConcurrentFactor(mbusParams.maxConcurrentFactor);
+ if (mbusParams.documentExpansionFactor != null)
+ builder.documentExpansionFactor(mbusParams.documentExpansionFactor);
+ if (mbusParams.containerCoreMemory != null)
+ builder.containerCoreMemory(mbusParams.containerCoreMemory);
+ }
+ if (containerDocproc != null)
+ containerDocproc.getConfig(builder);
+ }
+
+ public void setMbusParams(MbusParams mbusParams) {
+ this.mbusParams = mbusParams;
+ }
+
+ public void initialize(Map<String, AbstractSearchCluster> clusterMap) {
+ if (containerSearch != null)
+ containerSearch.connectSearchClusters(clusterMap);
+ }
+
+ public void addDefaultSearchAccessLog() {
+ addComponent(new AccessLogComponent(AccessLogComponent.AccessLogType.queryAccessLog, getName()));
+ }
+
+ @Override
+ public void getConfig(IlscriptsConfig.Builder builder) {
+ List<AbstractSearchCluster> searchClusters = new ArrayList<>();
+ searchClusters.addAll(Content.getSearchClusters(getRoot().configModelRepo()));
+ for (AbstractSearchCluster searchCluster : searchClusters) {
+ searchCluster.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(MetricDefaultsConfig.Builder builder) {
+ if (defaultMetricConsumerFactory != null) {
+ builder.factory(defaultMetricConsumerFactory);
+ }
+ }
+
+ @Override
+ public void getConfig(ClusterInfoConfig.Builder builder) {
+ builder.clusterId(name);
+ builder.nodeCount(containers.size());
+
+ for (Service service : getDescendantServices()) {
+ builder.services.add(new ClusterInfoConfig.Services.Builder()
+ .index(Integer.parseInt(service.getServicePropertyString("index", "99999")))
+ .hostname(service.getHostName())
+ .ports(getPorts(service)));
+ }
+ }
+
+ /**
+ * Returns a config server config containing the right zone settings (and defaults for the rest).
+ * This is useful to allow applications to find out in which zone they are runnung by having the Zone
+ * object (which is constructed from this config) injected.
+ */
+ @Override
+ public void getConfig(ConfigserverConfig.Builder builder) {
+ builder.environment(zone.environment().value());
+ builder.region(zone.region().value());
+ }
+
+ private List<ClusterInfoConfig.Services.Ports.Builder> getPorts(Service service) {
+ List<ClusterInfoConfig.Services.Ports.Builder> builders = new ArrayList<>();
+ PortsMeta portsMeta = service.getPortsMeta();
+ for (int i = 0; i < portsMeta.getNumPorts(); i++) {
+ builders.add(new ClusterInfoConfig.Services.Ports.Builder()
+ .number(service.getRelativePort(i))
+ .tags(ApplicationConfigProducerRoot.getPortTags(portsMeta, i))
+ );
+ }
+ return builders;
+ }
+
+ public void setDefaultMetricConsumerFactory(MetricDefaultsConfig.Factory.Enum defaultMetricConsumerFactory) {
+ Objects.requireNonNull(defaultMetricConsumerFactory, "defaultMetricConsumerFactory");
+ this.defaultMetricConsumerFactory = defaultMetricConsumerFactory;
+ }
+
+ @Override
+ public void getConfig(RoutingProviderConfig.Builder builder) {
+ builder.enabled(isHostedVespa());
+ }
+
+ public static class MbusParams {
+ //the amount of the maxpendingbytes to process concurrently, typically 0.2 (20%)
+ public final Double maxConcurrentFactor;
+
+ //the amount that documents expand temporarily when processing them
+ public final Double documentExpansionFactor;
+
+ //the space to reserve for container, docproc stuff (memory that cannot be used for processing documents), in MB
+ public final Integer containerCoreMemory;
+
+ public MbusParams(Double maxConcurrentFactor, Double documentExpansionFactor, Integer containerCoreMemory) {
+ this.maxConcurrentFactor = maxConcurrentFactor;
+ this.documentExpansionFactor = documentExpansionFactor;
+ this.containerCoreMemory = containerCoreMemory;
+ }
+ }
+
+ public Map<String, String> concreteDocumentTypes() {
+ return concreteDocumentTypes;
+ }
+
+ /**
+ * The configured service aliases for the service in this cluster
+ * @return alias list
+ */
+ public List<String> serviceAliases() {
+ return serviceAliases;
+ }
+
+ /**
+ * The configured endpoint aliases (fqdn) for the service in this cluster
+ * @return alias list
+ */
+ public List<String> endpointAliases() {
+ return endpointAliases;
+ }
+
+ @Override
+ public String toString() {
+ return "container cluster '" + getName() + "'";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerModel.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerModel.java
new file mode 100644
index 00000000000..29846ded00f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerModel.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+
+/**
+ * A model of a container cluster.
+ *
+ * @author tonytv
+ */
+public class ContainerModel extends ConfigModel {
+
+ // TODO: Move to referer
+ public static final String DOCPROC_RESERVED_NAME = "docproc";
+
+ private ContainerCluster containerCluster;
+
+ public ContainerModel(ConfigModelContext context) {
+ super(context);
+ }
+
+ public void setCluster(ContainerCluster containerCluster) { this.containerCluster = containerCluster; }
+
+ public ContainerCluster getCluster() { return containerCluster; }
+
+ @Override
+ public void prepare(ConfigModelRepo plugins) {
+ assert (getCluster() != null) : "Null container cluster!";
+ getCluster().prepare();
+ }
+
+ @Override
+ public void initialize(ConfigModelRepo configModelRepo) {
+ List<AbstractSearchCluster> searchClusters = Content.getSearchClusters(configModelRepo);
+
+ Map<String, AbstractSearchCluster> searchClustersByName = new TreeMap<>();
+ for (AbstractSearchCluster c : searchClusters) {
+ searchClustersByName.put(c.getClusterName(), c);
+ }
+
+ getCluster().initialize(searchClustersByName);
+ }
+
+ public static Collection<ContainerCluster> containerClusters(ConfigModelRepo models) {
+ List<ContainerCluster> containerClusters = new ArrayList<>();
+
+ for (ContainerModel model: models.getModels(ContainerModel.class))
+ containerClusters.add(model.getCluster());
+
+ return containerClusters;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java
new file mode 100644
index 00000000000..75ca2c1e958
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/AccessLogComponent.java
@@ -0,0 +1,99 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.container.core.AccessLogConfig;
+import com.yahoo.container.logging.VespaAccessLog;
+import com.yahoo.container.logging.YApacheAccessLog;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import static com.yahoo.container.core.AccessLogConfig.FileHandler.RotateScheme;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ * @since 5.1.4
+ */
+public final class AccessLogComponent extends SimpleComponent implements AccessLogConfig.Producer {
+
+
+ public enum AccessLogType { queryAccessLog, yApacheAccessLog }
+
+ private final String fileNamePattern;
+ private final String rotationInterval;
+ private final RotateScheme.Enum rotationScheme;
+ private final String symlinkName;
+
+ public AccessLogComponent(AccessLogType logType, String clusterName) {
+ this(logType,
+ String.format("logs/vespa/qrs/%s.%s.%s", capitalize(logType.name()), clusterName, "%Y%m%d%H%M%S"),
+ null, null,
+ capitalize(logType.name()) + "." + clusterName);
+ }
+
+ private static String capitalize(String name) {
+ return name.substring(0, 1).toUpperCase() + name.substring(1);
+ }
+
+ public AccessLogComponent(AccessLogType logType,
+ String fileNamePattern,
+ String rotationInterval,
+ RotateScheme.Enum rotationScheme, String symlinkName) {
+ super(new ComponentModel(accessLogClass(logType), null, "container-core", null));
+ this.fileNamePattern = fileNamePattern;
+ this.rotationInterval = rotationInterval;
+ this.rotationScheme = rotationScheme;
+ this.symlinkName = symlinkName;
+
+ if (fileNamePattern == null)
+ throw new RuntimeException("File name pattern required when configuring access log.");
+ }
+
+ private static String accessLogClass(AccessLogType logType) {
+ switch (logType) {
+ case yApacheAccessLog:
+ return YApacheAccessLog.class.getName();
+ case queryAccessLog:
+ return VespaAccessLog.class.getName();
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ @Override
+ public void getConfig(AccessLogConfig.Builder builder) {
+ builder.fileHandler(fileHandlerConfig());
+ }
+
+ private AccessLogConfig.FileHandler.Builder fileHandlerConfig() {
+ AccessLogConfig.FileHandler.Builder builder = new AccessLogConfig.FileHandler.Builder();
+ if (fileNamePattern != null)
+ builder.pattern(fileNamePattern);
+ if (rotationInterval != null)
+ builder.rotation(rotationInterval);
+ if (rotationScheme != null)
+ builder.rotateScheme(rotationScheme);
+ if (symlinkName != null)
+ builder.symlink(symlinkName);
+
+ return builder;
+ }
+
+ public String getFileNamePattern() {
+ return fileNamePattern;
+ }
+
+ public static final RotateScheme.Enum rotateScheme(@Nullable String name) {
+ if (name == null)
+ return null;
+
+ switch (name) {
+ case "date":
+ return RotateScheme.Enum.DATE;
+ case "sequence":
+ return RotateScheme.Enum.SEQUENCE;
+ default:
+ throw new IllegalArgumentException("Invalid rotation scheme " + name);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/Component.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Component.java
new file mode 100644
index 00000000000..8f83978ce02
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Component.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.osgi.provider.model.ComponentModel;
+
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ */
+public class Component<CHILD extends AbstractConfigProducer<?>, MODEL extends ComponentModel>
+ extends AbstractConfigProducer<CHILD> implements Comparable<Component<?, ?>> {
+
+ public final MODEL model;
+ final Set<Pair<String, Component>> injectedComponents = new LinkedHashSet<>();
+
+ public Component(MODEL model) {
+ super(model.getComponentId().stringValue());
+ this.model = model;
+ }
+
+ public ComponentId getGlobalComponentId() {
+ return model.getComponentId();
+ }
+
+ public ComponentId getComponentId() {
+ return model.getComponentId();
+ }
+
+ public ComponentSpecification getClassId() {
+ return model.getClassId();
+ }
+
+ public void inject(Component component) {
+ injectForName("", component);
+ }
+
+ public void injectForName(String name, Component component) {
+ injectedComponents.add(new Pair<>(name, component));
+ }
+
+ public void addComponent(CHILD child) {
+ addChild(child);
+ }
+
+ /** For testing only */
+ public Set<String> getInjectedComponentIds() {
+ Set<String> injectedIds = new HashSet<>();
+ for (Pair<String, Component> injected : injectedComponents) {
+ injectedIds.add(injected.getSecond().getSubId());
+ }
+ return injectedIds;
+ }
+
+ @Override
+ public int compareTo(Component<?, ?> other) {
+ return getComponentId().compareTo(other.getComponentId());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentGroup.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentGroup.java
new file mode 100644
index 00000000000..dda21bd80da
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentGroup.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * @author tonytv
+ */
+public class ComponentGroup <CHILD extends Component<?, ?>> extends ConfigProducerGroup<CHILD> {
+
+ public ComponentGroup(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ }
+
+ public void addComponent(CHILD producer) {
+ super.addComponent(producer.getComponentId(), producer);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentsConfigGenerator.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentsConfigGenerator.java
new file mode 100644
index 00000000000..a800daedd38
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ComponentsConfigGenerator.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static com.yahoo.container.ComponentsConfig.Components;
+
+/**
+ * @author gjoranv
+ */
+public class ComponentsConfigGenerator {
+
+ public static List<Components.Builder> generate(Collection<? extends Component<?, ?>> components) {
+ List<Components.Builder> result = new ArrayList<>();
+
+ for (Component component : components) {
+ result.add(componentsConfig(component));
+ }
+ return result;
+ }
+
+ public static Components.Builder componentsConfig(Component<?, ?> component) {
+ Components.Builder builder = new Components.Builder();
+ builder.id(component.getGlobalComponentId().stringValue());
+ builder.configId(component.getConfigId());
+
+
+ bundleInstantiationSpecification(builder, component.model.bundleInstantiationSpec);
+ builder.inject.addAll(componentsToInject(component.injectedComponents));
+
+ return builder;
+ }
+
+ private static void bundleInstantiationSpecification(Components.Builder config, BundleInstantiationSpecification spec) {
+ config.classId(spec.classId.stringValue());
+ config.bundle(spec.bundle.stringValue());
+ }
+
+
+ private static List<Components.Inject.Builder> componentsToInject(
+ Collection<Pair<String, Component>> injectedComponents) {
+
+ List<Components.Inject.Builder> result = new ArrayList<>();
+
+ for (Pair<String, Component> injected : injectedComponents) {
+ Components.Inject.Builder builder = new Components.Inject.Builder();
+
+ builder.id(injected.getSecond().getGlobalComponentId().stringValue());
+ builder.name(injected.getFirst());
+ result.add(builder);
+ }
+ return result;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConfigProducerGroup.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConfigProducerGroup.java
new file mode 100644
index 00000000000..d3311806d69
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConfigProducerGroup.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+import java.util.*;
+
+/**
+ * A group of config producers that have a component id.
+ *
+ * @author tonytv
+ */
+public class ConfigProducerGroup<CHILD extends AbstractConfigProducer<?>> extends AbstractConfigProducer<CHILD> {
+
+ private final Map<ComponentId, CHILD> producerById = new LinkedHashMap<>();
+
+ public ConfigProducerGroup(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ }
+
+ public void addComponent(ComponentId id, CHILD producer) {
+ boolean wasAdded = producerById.put(id, producer) == null;
+ if (!wasAdded) {
+ throw new IllegalArgumentException("Two entities have the same component id '" +
+ id + "' in the same scope.");
+ }
+ addChild(producer);
+ }
+
+ /**
+ * Removes a component by id
+ *
+ * @return the removed component, or null if it was not present
+ */
+ public CHILD removeComponent(ComponentId componentId) {
+ CHILD component = producerById.remove(componentId);
+ if (component == null) return null;
+ removeChild(component);
+ return component;
+ }
+
+ public Collection<CHILD> getComponents() {
+ return Collections.unmodifiableCollection(getChildren().values());
+ }
+
+ public <T extends CHILD> Collection<T> getComponents(Class<T> componentClass) {
+ Collection<T> result = new ArrayList<>();
+
+ for (CHILD child: getChildren().values()) {
+ if (componentClass.isInstance(child)) {
+ result.add(componentClass.cast(child));
+ }
+ }
+ return Collections.unmodifiableCollection(result);
+ }
+
+ /**
+ * @return A map of all components in this group, with (local) component ID as key.
+ */
+ public Map<ComponentId, CHILD> getComponentMap() {
+ return Collections.unmodifiableMap(producerById);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ContainerSubsystem.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ContainerSubsystem.java
new file mode 100644
index 00000000000..9880aa49a95
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ContainerSubsystem.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.chain.Chains;
+import edu.umd.cs.findbugs.annotations.NonNull;
+
+/**
+ * Holder for components and options related to either processing/search/docproc
+ * for a container cluster.
+ *
+ * @author gjoranv
+ * @since 5.1.9
+ */
+public abstract class ContainerSubsystem<CHAINS extends Chains<?>> {
+
+ private final CHAINS chains;
+
+ public ContainerSubsystem(CHAINS chains) {
+ this.chains = chains;
+ }
+
+ @NonNull
+ public CHAINS getChains() {
+ if (chains == null)
+ throw new IllegalStateException("Null chains for " + this);
+ return chains;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/DiscBindingsConfigGenerator.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/DiscBindingsConfigGenerator.java
new file mode 100644
index 00000000000..6c03c64ce02
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/DiscBindingsConfigGenerator.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import java.util.*;
+
+import static com.yahoo.container.jdisc.JdiscBindingsConfig.Handlers;
+
+/**
+ * @author gjoranv
+ * @since 5.1.8
+ */
+public class DiscBindingsConfigGenerator {
+
+ public static Map<String, Handlers.Builder> generate(Collection<? extends Handler<?>> handlers) {
+ Map<String, Handlers.Builder> handlerBuilders = new LinkedHashMap<>();
+
+ for (Handler<?> handler : handlers) {
+ handlerBuilders.putAll(generate(handler));
+ }
+ return handlerBuilders;
+ }
+
+ public static <T extends Handler<?>> Map<String, Handlers.Builder> generate(T handler) {
+ if (handler.getServerBindings().isEmpty() && handler.getClientBindings().isEmpty())
+ return Collections.emptyMap();
+
+ return Collections.singletonMap(handler.model.getComponentId().stringValue(),
+ new Handlers.Builder()
+ .serverBindings(handler.getServerBindings())
+ .clientBindings(handler.getClientBindings()));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/FileStatusHandlerComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/FileStatusHandlerComponent.java
new file mode 100644
index 00000000000..758806395a6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/FileStatusHandlerComponent.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.container.core.VipStatusConfig;
+import com.yahoo.osgi.provider.model.ComponentModel;
+
+/**
+ * Sets up VipStatusHandler that answers OK when a certain file is present.
+ * @author tonytv
+ */
+public class FileStatusHandlerComponent extends Handler implements VipStatusConfig.Producer {
+ private final String fileName;
+
+ public FileStatusHandlerComponent(String id, String fileName, String... bindings) {
+ super(new ComponentModel(id, "com.yahoo.container.handler.VipStatusHandler", null, null));
+
+ this.fileName = fileName;
+ addServerBindings(bindings);
+ }
+
+ @Override
+ public void getConfig(VipStatusConfig.Builder builder) {
+ builder.accessdisk(true).
+ statusfile(fileName);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java
new file mode 100644
index 00000000000..0a9657a2d9a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Handler.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <p>
+ * Models a jdisc RequestHandler (including ClientProvider).
+ * RequestHandlers always have at least one server binding,
+ * while ClientProviders have at least one client binding.
+ * </p>
+ * <p>
+ * Note that this is also used to model vespa handlers (which do not have any bindings)
+ * </p>
+ *
+ * @author gjoranv
+ * @since 5.1.6
+ */
+public class Handler<CHILD extends AbstractConfigProducer<?>> extends Component<CHILD, ComponentModel> {
+
+ private List<String> serverBindings = new ArrayList<>();
+ private List<String> clientBindings = new ArrayList<>();
+
+ public Handler(ComponentModel model) {
+ super(model);
+ }
+
+ public static Handler<AbstractConfigProducer<?>> fromClassName(String className) {
+ return new Handler<>(new ComponentModel(className, null, null, null));
+ }
+
+ public static Handler<AbstractConfigProducer<?>> getVespaHandlerFromClassName(String className) {
+ return new Handler<>(new ComponentModel(BundleInstantiationSpecification.getInternalHandlerSpecificationFromStrings(className, null), null));
+ }
+
+ public void addServerBindings(String... bindings) {
+ serverBindings.addAll(Arrays.asList(bindings));
+ }
+
+ public void addClientBindings(String... bindings) {
+ clientBindings.addAll(Arrays.asList(bindings));
+ }
+
+ public final List<String> getServerBindings() {
+ return Collections.unmodifiableList(serverBindings);
+ }
+
+ public final List<String> getClientBindings() {
+ return Collections.unmodifiableList(clientBindings);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/HttpFilter.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/HttpFilter.java
new file mode 100644
index 00000000000..b919d8bd70e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/HttpFilter.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.FilterConfigProvider;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+
+/**
+ * This is only for the legacy yca filter setup, outside http.
+ *
+ * TODO: Remove when 'filter' directly under 'jdisc' can be removed from services.xml
+ *
+ * @author tonytv
+ */
+public class HttpFilter extends SimpleComponent {
+ private static final ComponentSpecification filterConfigProviderClass =
+ ComponentSpecification.fromString(FilterConfigProvider.class.getName());
+
+ public final SimpleComponent filterConfigProvider;
+
+ public HttpFilter(BundleInstantiationSpecification spec) {
+ super(new ComponentModel(spec));
+
+ filterConfigProvider = new SimpleComponent(new ComponentModel(
+ new BundleInstantiationSpecification(configProviderId(spec.id), filterConfigProviderClass, null)));
+
+ addChild(filterConfigProvider);
+ }
+
+ // public for testing
+ public static ComponentId configProviderId(ComponentId filterId) {
+ return ComponentId.fromString("filterConfig").nestInNamespace(filterId);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/Servlet.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Servlet.java
new file mode 100644
index 00000000000..68ba3436209
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/Servlet.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.jdisc.http.ServletPathsConfig;
+import com.yahoo.osgi.provider.model.ComponentModel;
+
+/**
+ * @author stiankri
+ */
+public class Servlet extends SimpleComponent {
+ private final String bindingPath;
+
+ public Servlet(ComponentModel componentModel, String bindingPath) {
+ super(componentModel);
+ this.bindingPath = bindingPath;
+ }
+
+ public ServletPathsConfig.Servlets.Builder toConfigBuilder() {
+ return new ServletPathsConfig.Servlets.Builder()
+ .path(bindingPath);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ServletProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ServletProvider.java
new file mode 100644
index 00000000000..7c6bc169878
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ServletProvider.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.servlet.ServletConfigConfig;
+import com.yahoo.osgi.provider.model.ComponentModel;
+
+import java.util.Map;
+
+/**
+ * @author stiankri
+ */
+public class ServletProvider extends Servlet implements ServletConfigConfig.Producer {
+ public static final String BUNDLE = "container-core";
+ public static final String CLASS = "com.yahoo.container.servlet.ServletProvider";
+
+ private static final ComponentId SERVLET_PROVIDER_NAMESPACE = ComponentId.fromString("servlet-provider");
+ private final Map<String, String> servletConfig;
+
+ public ServletProvider(SimpleComponent servletToProvide, String bindingPath, Map<String, String> servletConfig) {
+ super(new ComponentModel(
+ new BundleInstantiationSpecification(servletToProvide.getComponentId().nestInNamespace(SERVLET_PROVIDER_NAMESPACE),
+ ComponentSpecification.fromString(CLASS),
+ ComponentSpecification.fromString(BUNDLE))),
+ bindingPath);
+
+ inject(servletToProvide);
+ addChild(servletToProvide);
+ this.servletConfig = servletConfig;
+ }
+
+ @Override
+ public void getConfig(ServletConfigConfig.Builder builder) {
+ builder.map(servletConfig);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/SimpleComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/SimpleComponent.java
new file mode 100644
index 00000000000..a0835f3ce88
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/SimpleComponent.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * A component that only needs a simple ComponentModel.
+ *
+ * @author gjoranv
+ * @since 5.1.9
+ */
+public class SimpleComponent extends Component<AbstractConfigProducer<?>, ComponentModel> {
+
+ public SimpleComponent(ComponentModel model) {
+ super(model);
+ }
+
+ // @Convenience // For a component that uses the class name as id.
+ public SimpleComponent(String className) {
+ this(new ComponentModel(BundleInstantiationSpecification.getFromStrings(className, null, null)));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/StatisticsComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/StatisticsComponent.java
new file mode 100644
index 00000000000..b459acd63e9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/StatisticsComponent.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.vespa.model.admin.MonitoringSystem;
+import com.yahoo.container.StatisticsConfig;
+
+/**
+ * @author tonytv
+ */
+public class StatisticsComponent extends SimpleComponent implements StatisticsConfig.Producer {
+
+ public StatisticsComponent() {
+ super("com.yahoo.statistics.StatisticsImpl");
+ }
+
+ @Override
+ public void getConfig(StatisticsConfig.Builder builder) {
+ MonitoringSystem monitoringSystem = getMonitoringService();
+ if (monitoringSystem != null) {
+ builder.
+ collectionintervalsec(monitoringSystem.getIntervalSeconds().doubleValue()).
+ loggingintervalsec(monitoringSystem.getIntervalSeconds().doubleValue());
+ }
+ builder.values(new StatisticsConfig.Values.Builder().
+ name("query_latency").
+ operations(new StatisticsConfig.Values.Operations.Builder().
+ name(StatisticsConfig.Values.Operations.Name.REGULAR).
+ arguments(new StatisticsConfig.Values.Operations.Arguments.Builder().
+ key("limits").
+ value("25,50,100,500"))));
+
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chain.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chain.java
new file mode 100644
index 00000000000..558fe368786
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chain.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component.chain;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.ComponentGroup;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import static com.yahoo.container.core.ChainsConfig.Chains.Type;
+
+/**
+ * Represents a component chain in the vespa model.
+ * The inner components are represented as children.
+ *
+ * @author tonytv
+ * @author gjoranv
+ */
+public class Chain<T extends ChainedComponent<?>> extends AbstractConfigProducer<AbstractConfigProducer<?>> {
+
+ private final ComponentId componentId;
+ private final ChainSpecification specWithoutInnerComponents;
+ private final ComponentGroup<T> innerComponentsGroup;
+ private static final Type.Enum TYPE = Type.SEARCH;
+
+
+ public Chain(ChainSpecification specWithoutInnerComponents) {
+ super(specWithoutInnerComponents.componentId.stringValue());
+
+ this.componentId = specWithoutInnerComponents.componentId;
+ this.specWithoutInnerComponents = specWithoutInnerComponents;
+ assertNoInnerComponents(specWithoutInnerComponents);
+
+ innerComponentsGroup = new ComponentGroup<>(this, "component");
+ }
+
+ private void assertNoInnerComponents(ChainSpecification specWithoutInnerComponents) {
+ for (ComponentSpecification component : specWithoutInnerComponents.componentReferences) {
+ assert (component.getNamespace() == null);
+ }
+ }
+
+ public void addInnerComponent(T component) {
+ innerComponentsGroup.addComponent(component);
+ }
+
+ public ChainSpecification getChainSpecification() {
+ Collection<ComponentSpecification> innerComponentSpecifications = new ArrayList<>();
+
+ for (ChainedComponent innerComponent : getInnerComponents()) {
+ innerComponentSpecifications.add(innerComponent.getGlobalComponentId().toSpecification());
+ }
+
+ return specWithoutInnerComponents.
+ addComponents(innerComponentSpecifications).
+ setComponentId(getGlobalComponentId());
+ }
+
+ public Collection<T> getInnerComponents() {
+ return innerComponentsGroup.getComponents();
+ }
+
+ public ComponentId getGlobalComponentId() {
+ return componentId;
+ }
+
+ public final ComponentId getId() { return getGlobalComponentId(); }
+
+ public final ComponentId getComponentId() {
+ return componentId;
+ }
+
+ // TODO: remove when DocumentProcessingHandler takes its own version of the chains config as ctor arg
+ public Type.Enum getType() {
+ return TYPE;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponent.java
new file mode 100644
index 00000000000..90579e8b77c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponent.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component.chain;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.Component;
+
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ *
+ * Base class for all ChainedComponent config producers.
+ */
+public class ChainedComponent<T extends ChainedComponentModel> extends Component<AbstractConfigProducer<?>, T> {
+
+ public ChainedComponent(T model) {
+ super(model);
+ }
+
+ public void initialize() {}
+
+ @Override
+ public ComponentId getGlobalComponentId() {
+ return model.getComponentId().nestInNamespace(namespace());
+ }
+
+ private ComponentId namespace() {
+ AbstractConfigProducer owner = getParent().getParent();
+ return (owner instanceof Chain) ?
+ ((Chain) owner).getGlobalComponentId() :
+ null;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponentConfigGenerator.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponentConfigGenerator.java
new file mode 100644
index 00000000000..34aee0e217e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainedComponentConfigGenerator.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component.chain;
+
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.container.core.ChainsConfig;
+
+import java.util.Set;
+
+import static com.yahoo.container.core.ChainsConfig.Components;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ *
+ * Generates config for all the chained components.
+ */
+class ChainedComponentConfigGenerator {
+
+ public static void generate(ChainsConfig.Builder builder, Set<? extends ChainedComponent> components) {
+ for (ChainedComponent<ChainedComponentModel> component : components) {
+ builder.components(getComponent(component));
+ }
+ }
+
+ private static Components.Builder getComponent(ChainedComponent<ChainedComponentModel> component) {
+ return new Components.Builder()
+ .id(component.getGlobalComponentId().stringValue())
+ .dependencies(getDependencies(component));
+ }
+
+ private static Components.Dependencies.Builder getDependencies(ChainedComponent<ChainedComponentModel> component) {
+ Dependencies dependencies = component.model.dependencies;
+ return new Components.Dependencies.Builder()
+ .provides(dependencies.provides())
+ .before(dependencies.before())
+ .after(dependencies.after());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chains.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chains.java
new file mode 100644
index 00000000000..bf8e611bfe8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/Chains.java
@@ -0,0 +1,107 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component.chain;
+
+import com.yahoo.component.chain.model.ChainsModel;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.container.core.ChainsConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.ComponentGroup;
+import com.yahoo.vespa.model.container.component.ConfigProducerGroup;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Root config producer the whole chains model(contains chains and components).
+ * @author tonytv
+ * @author gjoranv
+ */
+public class Chains<CHAIN extends Chain<?>>
+ extends AbstractConfigProducer<AbstractConfigProducer<?>>
+ implements ChainsConfig.Producer {
+
+ private final ComponentGroup<ChainedComponent<?>> componentGroup;
+ private final ConfigProducerGroup<CHAIN> chainGroup;
+
+ public Chains(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ componentGroup = new ComponentGroup<>(this, "component");
+ chainGroup = new ConfigProducerGroup<>(this, "chain");
+ }
+
+ public void initializeComponents() {
+ for (ChainedComponent component : allComponents()) {
+ component.initialize();
+ }
+ }
+
+ public void validate() throws Exception {
+ ChainsModel chainsModel = new ChainsModel();
+
+ for (CHAIN chain : allChains().allComponents()) {
+ chainsModel.register(chain.getChainSpecification());
+ }
+ for (ChainedComponent<?> component : allComponents()) {
+ chainsModel.register(component.getGlobalComponentId(), component.model);
+ }
+ chainsModel.validate();
+
+ super.validate();
+ }
+
+ public Set<ChainedComponent<?>> allComponents() {
+ Set<ChainedComponent<?>> result = new LinkedHashSet<>();
+ result.addAll(componentGroup.getComponents());
+
+ for (CHAIN chain : allChains().allComponents()) {
+ result.addAll(chain.getInnerComponents());
+ }
+ return result;
+ }
+
+ public ComponentRegistry<ChainedComponent<?>> componentsRegistry() {
+ ComponentRegistry<ChainedComponent<?>> result = new ComponentRegistry<>();
+
+ for (ChainedComponent<?> component: componentGroup.getComponents())
+ result.register(component.getGlobalComponentId(), component);
+
+ for (CHAIN chain : allChains().allComponents()) {
+ for (ChainedComponent<?> component: chain.getInnerComponents()) {
+ result.register(component.getGlobalComponentId(), component);
+ }
+ }
+ return result;
+ }
+
+ public ComponentRegistry<CHAIN> allChains() {
+ ComponentRegistry<CHAIN> allChains = new ComponentRegistry<>();
+ for (CHAIN chain : chainGroup.getComponents()) {
+ allChains.register(chain.getId(), chain);
+ }
+ allChains.freeze();
+ return allChains;
+ }
+
+ public void add(CHAIN chain) {
+ chainGroup.addComponent(chain.getId(), chain);
+ }
+
+ public void add(ChainedComponent outerComponent) {
+ componentGroup.addComponent(outerComponent);
+ }
+
+ @Override
+ public void getConfig(ChainsConfig.Builder builder) {
+ ChainsConfigGenerator.generate(builder, allChains().allComponents());
+ ChainedComponentConfigGenerator.generate(builder, allComponents());
+ }
+
+ public ConfigProducerGroup<ChainedComponent<?>> getComponentGroup() {
+ return componentGroup;
+ }
+
+ protected ConfigProducerGroup<CHAIN> getChainGroup() {
+ return chainGroup;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainsConfigGenerator.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainsConfigGenerator.java
new file mode 100644
index 00000000000..da3f8846974
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ChainsConfigGenerator.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component.chain;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.container.core.ChainsConfig;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static com.yahoo.container.core.ChainsConfig.Chains.*;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ *
+ * Generates config for a all the chains.
+ */
+class ChainsConfigGenerator<T extends Chain> {
+
+ public static <T extends Chain> void generate(ChainsConfig.Builder builder, Collection<T> chains) {
+ for (T chain : chains) {
+ builder.chains(getChain(chain));
+ }
+ }
+
+ private static <T extends Chain> ChainsConfig.Chains.Builder getChain(T chain) {
+ ChainSpecification specification = chain.getChainSpecification();
+
+ return new ChainsConfig.Chains.Builder()
+ .type(chain.getType())
+ .id(specification.componentId.stringValue())
+ .components(getComponents(specification.componentReferences))
+ .inherits(getComponents(specification.inheritance.chainSpecifications))
+ .excludes(getComponents(specification.inheritance.excludedComponents))
+ .phases(getPhases(specification.phases()));
+ }
+
+ private static List<String> getComponents(Collection<ComponentSpecification> componentSpecs) {
+ List<String> components = new ArrayList<>();
+ for (ComponentSpecification spec : componentSpecs)
+ components.add(spec.stringValue());
+ return components;
+ }
+
+ private static List<Phases.Builder> getPhases(Collection<Phase> phases) {
+ List<Phases.Builder> builders = new ArrayList<>();
+ for (Phase phase : phases) {
+ builders.add(
+ new Phases.Builder()
+ .id(phase.getName())
+ .before(phase.before())
+ .after(phase.after()));
+ }
+ return builders;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ProcessingHandler.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ProcessingHandler.java
new file mode 100644
index 00000000000..ecd1e45c7fa
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/ProcessingHandler.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.component.chain;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.core.ChainsConfig;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.Handler;
+
+
+/**
+ * Represents a handler for processing chains.
+ *
+ * @author gjoranv
+ * @since 5.1.7
+ */
+public class ProcessingHandler<CHAINS extends Chains<?>>
+ extends Handler<AbstractConfigProducer<?>>
+ implements ChainsConfig.Producer {
+
+ protected final CHAINS chains;
+
+ public ProcessingHandler(CHAINS chains, String handlerClass) {
+ super(new ComponentModel(BundleInstantiationSpecification.getInternalProcessingSpecificationFromStrings(handlerClass, null), null));
+ this.chains = chains;
+
+ }
+
+ @Override
+ public void getConfig(ChainsConfig.Builder builder) {
+ chains.getConfig(builder);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/package-info.java
new file mode 100644
index 00000000000..c35c50bfde9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/chain/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.container.component.chain;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/package-info.java
new file mode 100644
index 00000000000..4471383c95e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.container.component;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java
new file mode 100644
index 00000000000..c52a7375528
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/ConfigserverCluster.java
@@ -0,0 +1,181 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.configserver;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.cloud.config.ZookeeperServerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.StatisticsConfig;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.jdisc.metrics.yamasconsumer.cloud.ScoreBoardConfig;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.configserver.option.CloudConfigOptions;
+import com.yahoo.vespa.model.container.configserver.option.CloudConfigOptions.ConfigServer;
+
+import java.util.Optional;
+
+/**
+ * Represents a config server cluster.
+ *
+ * @author lulf
+ * @since 5.15
+ */
+public class ConfigserverCluster extends AbstractConfigProducer
+ implements
+ ZookeeperServerConfig.Producer,
+ ConfigserverConfig.Producer,
+ ScoreBoardConfig.Producer,
+ StatisticsConfig.Producer,
+ HealthMonitorConfig.Producer {
+ private final CloudConfigOptions options;
+ private ContainerCluster containerCluster;
+
+ public ConfigserverCluster(AbstractConfigProducer parent, String subId, CloudConfigOptions options) {
+ super(parent, subId);
+ this.options = options;
+ }
+
+ public void setContainerCluster(ContainerCluster containerCluster) {
+ this.containerCluster = containerCluster;
+
+ // If we are in a config server cluster the correct zone is propagated through cloud config options,
+ // not through config to deployment options (see StandaloneContainerApplication.scala),
+ // so we need to propagate the zone options into the container from here
+ Environment environment = options.environment().isPresent() ? Environment.from(options.environment().get()) : Environment.defaultEnvironment();
+ RegionName region = options.region().isPresent() ? RegionName.from(options.region().get()) : RegionName.defaultName();
+ containerCluster.setZone(new Zone(environment, region));
+ }
+
+ @Override
+ public void getConfig(ZookeeperServerConfig.Builder builder) {
+ String myhostname = HostName.getLocalhost();
+ int myid = 0;
+ int i = 0;
+ for (ConfigServer server : getConfigServers()) {
+ if (server.hostName.equals(myhostname)) {
+ myid = i;
+ }
+ builder.server(getZkServer(server, i));
+ i++;
+ }
+ builder.myid(myid);
+ if (options.zookeeperClientPort().isPresent()) {
+ builder.clientPort(options.zookeeperClientPort().get());
+ }
+ }
+
+ @Override
+ public void getConfig(ConfigserverConfig.Builder builder) {
+ for (String pluginDir : getConfigModelPluginDirs()) {
+ builder.configModelPluginDir(pluginDir);
+ }
+ if (options.sessionLifeTimeSecs().isPresent()) {
+ builder.sessionLifetime(options.sessionLifeTimeSecs().get());
+ }
+ if (options.zookeeperBarrierTimeout().isPresent()) {
+ builder.zookeeper(new ConfigserverConfig.Zookeeper.Builder().barrierTimeout(options.zookeeperBarrierTimeout().get()));
+ }
+ if (options.rpcPort().isPresent()) {
+ builder.rpcport(options.rpcPort().get());
+ }
+ if (options.multiTenant().isPresent()) {
+ builder.multitenant(options.multiTenant().get());
+ }
+ if (options.payloadCompressionType().isPresent()) {
+ builder.payloadCompressionType(ConfigserverConfig.PayloadCompressionType.Enum.valueOf(options.payloadCompressionType().get()));
+ }
+ for (ConfigServer server : getConfigServers()) {
+ ConfigserverConfig.Zookeeperserver.Builder zkBuilder = new ConfigserverConfig.Zookeeperserver.Builder();
+ zkBuilder.hostname(server.hostName);
+ if (options.zookeeperClientPort().isPresent()) {
+ zkBuilder.port(options.zookeeperClientPort().get());
+ }
+ builder.zookeeperserver(zkBuilder);
+ }
+ if (options.environment().isPresent()) {
+ builder.environment(options.environment().get());
+ }
+ if (options.region().isPresent()) {
+ builder.region(options.region().get());
+ }
+ if (options.defaultFlavor().isPresent()) {
+ builder.defaultFlavor(options.defaultFlavor().get());
+ }
+ if (options.defaultAdminFlavor().isPresent()) {
+ builder.defaultAdminFlavor(options.defaultAdminFlavor().get());
+ }
+ if (options.defaultContainerFlavor().isPresent()) {
+ builder.defaultContainerFlavor(options.defaultContainerFlavor().get());
+ }
+ if (options.defaultContentFlavor().isPresent()) {
+ builder.defaultContentFlavor(options.defaultContentFlavor().get());
+ }
+
+ builder.serverId(HostName.getLocalhost());
+ if (!containerCluster.getHttp().getHttpServer().getConnectorFactories().isEmpty()) {
+ builder.httpport(containerCluster.getHttp().getHttpServer().getConnectorFactories().get(0).getListenPort());
+ }
+ if (options.useVespaVersionInRequest().isPresent()) {
+ builder.useVespaVersionInRequest(options.useVespaVersionInRequest().get());
+ } else if (options.multiTenant().isPresent()) {
+ builder.useVespaVersionInRequest(options.multiTenant().get());
+ }
+ if (options.hostedVespa().isPresent()) {
+ builder.hostedVespa(options.hostedVespa().get());
+ }
+ if (options.numParallelTenantLoaders().isPresent()) {
+ builder.numParallelTenantLoaders(options.numParallelTenantLoaders().get());
+ }
+ }
+
+ private String[] getConfigModelPluginDirs() {
+ if (options.configModelPluginDirs().length > 0) {
+ return options.configModelPluginDirs();
+ } else {
+ return new String[]{Defaults.getDefaults().vespaHome() + "lib/jars/config-models"};
+ }
+ }
+
+ private ConfigServer[] getConfigServers() {
+ if (options.allConfigServers().length > 0) {
+ return options.allConfigServers();
+ } else {
+ return new ConfigServer[]{new ConfigServer(HostName.getLocalhost(), Optional.<Integer>empty()) };
+ }
+ }
+
+ private ZookeeperServerConfig.Server.Builder getZkServer(ConfigServer server, int id) {
+ ZookeeperServerConfig.Server.Builder builder = new ZookeeperServerConfig.Server.Builder();
+ if (options.zookeeperElectionPort().isPresent()) {
+ builder.electionPort(options.zookeeperElectionPort().get());
+ }
+ if (options.zookeeperQuorumPort().isPresent()) {
+ builder.quorumPort(options.zookeeperQuorumPort().get());
+ }
+ builder.hostname(server.hostName);
+ builder.id(id);
+ return builder;
+ }
+
+ @Override
+ public void getConfig(ScoreBoardConfig.Builder builder) {
+ builder.applicationName("configserver");
+ builder.flushTime(60);
+ builder.step(60);
+ }
+
+ @Override
+ public void getConfig(StatisticsConfig.Builder builder) {
+ builder.collectionintervalsec(60.0);
+ builder.loggingintervalsec(60.0);
+ }
+
+ @Override
+ public void getConfig(HealthMonitorConfig.Builder builder) {
+ builder.snapshot_interval(60.0);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java
new file mode 100644
index 00000000000..3bb4f9d09f5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/CloudConfigOptions.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.configserver.option;
+
+import java.util.Optional;
+
+/**
+ * @author tonytv
+ */
+public interface CloudConfigOptions {
+
+ class ConfigServer {
+ public final String hostName;
+ public final Optional<Integer> port;
+
+ public ConfigServer(String hostName, Optional<Integer> port) {
+ this.hostName = hostName;
+ this.port = port;
+ }
+ }
+
+
+ Optional<Integer> rpcPort();
+ Optional<Boolean> multiTenant();
+ Optional<Boolean> hostedVespa();
+
+ ConfigServer[] allConfigServers();
+ Optional<Integer> zookeeperClientPort();
+ String[] configModelPluginDirs();
+ Optional<Long> sessionLifeTimeSecs();
+
+ //TODO: which unit?
+ Optional<Long> zookeeperBarrierTimeout();
+ Optional<Integer> zookeeperElectionPort();
+ Optional<Integer> zookeeperQuorumPort();
+ Optional<String> payloadCompressionType();
+ Optional<String> environment();
+ Optional<String> region();
+ Optional<String> defaultFlavor();
+ Optional<String> defaultAdminFlavor();
+ Optional<String> defaultContainerFlavor();
+ Optional<String> defaultContentFlavor();
+ Optional<Boolean> useVespaVersionInRequest();
+ Optional<Integer> numParallelTenantLoaders();
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/package-info.java
new file mode 100644
index 00000000000..a6f5bfa895a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/configserver/option/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author tonytv
+ */
+@ExportPackage
+package com.yahoo.vespa.model.container.configserver.option;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/ContainerDocproc.java b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/ContainerDocproc.java
new file mode 100644
index 00000000000..a1c98396ffc
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/ContainerDocproc.java
@@ -0,0 +1,178 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.docproc;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.docproc.DocprocConfig;
+import com.yahoo.container.jdisc.config.SessionConfig;
+import com.yahoo.container.jdisc.ContainerMbusConfig;
+import com.yahoo.config.docproc.SchemamappingConfig;
+import com.yahoo.docproc.jdisc.messagebus.MbusRequestContext;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.ContainerSubsystem;
+import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author einarmr
+ * @author gjoranv
+ * @since 5.1.9
+ */
+public class ContainerDocproc extends ContainerSubsystem<DocprocChains>
+ implements
+ ContainerMbusConfig.Producer,
+ SchemamappingConfig.Producer,
+ DocprocConfig.Producer
+{
+ public final Options options;
+
+ // Whether or not to prefer sending to a local node.
+ private boolean preferLocalNode = false;
+
+ // The number of nodes to use per client.
+ private int numNodesPerClient = 0;
+
+ private Map<Pair<String, String>, String> fieldNameSchemaMap = new HashMap<>();
+
+ public ContainerDocproc(ContainerCluster cluster, DocprocChains chains) {
+ this(cluster, chains, new Options(false, null, null, null, null, null, null));
+ }
+
+ public ContainerDocproc(ContainerCluster cluster, DocprocChains chains, Options options) {
+ this(cluster, chains, options, true);
+ }
+
+ private void addSource(
+ final ContainerCluster cluster, final String name, final SessionConfig.Type.Enum type) {
+ final MbusClient mbusClient = new MbusClient(name, type);
+ mbusClient.addClientBindings("mbus://*/" + mbusClient.getSessionName());
+ cluster.addComponent(mbusClient);
+ }
+
+ public ContainerDocproc(ContainerCluster cluster, DocprocChains chains, Options options, boolean addSourceClientProvider) {
+ super(chains);
+ assert (options != null) : "Null Options for " + this + " under cluster " + cluster.getName();
+ this.options = options;
+
+ if (addSourceClientProvider) {
+ addSource(cluster, "source", SessionConfig.Type.SOURCE);
+ addSource(cluster, MbusRequestContext.internalNoThrottledSource, SessionConfig.Type.INTERNAL);
+ }
+ }
+
+ public boolean isCompressDocuments() {
+ return options.compressDocuments;
+ }
+
+ public boolean isPreferLocalNode() {
+ return preferLocalNode;
+ }
+
+ public void setPreferLocalNode(boolean preferLocalNode) {
+ this.preferLocalNode = preferLocalNode;
+ }
+
+ public int getNumNodesPerClient() {
+ return numNodesPerClient;
+ }
+
+ public void setNumNodesPerClient(int numNodesPerClient) {
+ this.numNodesPerClient = numNodesPerClient;
+ }
+
+ @Override
+ public void getConfig(ContainerMbusConfig.Builder builder) {
+ builder.maxpendingcount(getMaxMessagesInQueue());
+ if (getMaxQueueMbSize() != null)
+ builder.maxpendingsize(getMaxQueueMbSize()); //yes, this shall be set in megabytes.
+ }
+
+ private int getMaxMessagesInQueue() {
+ if (options.maxMessagesInQueue != null) {
+ return options.maxMessagesInQueue;
+ }
+
+ //maxmessagesinqueue has not been set for this node. let's try to give a good value anyway:
+ return 2048 * getChains().allChains().allComponents().size();
+ //intentionally high, getMaxQueueMbSize() will probably kick in before this one!
+ }
+
+ @Nullable
+ private Integer getMaxQueueMbSize() {
+ return options.maxQueueMbSize;
+ }
+
+
+ private Integer getMaxQueueTimeMs() {
+ return options.maxQueueTimeMs;
+ }
+
+ @Override
+ public void getConfig(DocprocConfig.Builder builder) {
+ if (getMaxQueueTimeMs() != null) {
+ builder.maxqueuetimems(getMaxQueueTimeMs());
+ }
+ }
+
+ public ProcessingHandler<DocprocChains> getDocprocHandler() {
+ return ((DocprocChains) getChains()).getDocprocHandler();
+ }
+
+ @Override
+ public void getConfig(SchemamappingConfig.Builder builder) {
+ Map<Pair<String, String>, String> allMappings = new HashMap<>();
+ for (DocprocChain chain : getChains().allChains().allComponents()) {
+ for (DocumentProcessor processor : chain.getInnerComponents()) {
+ allMappings.putAll(fieldNameSchemaMap());
+ allMappings.putAll(chain.fieldNameSchemaMap());
+ allMappings.putAll(processor.fieldNameSchemaMap());
+ for (Map.Entry<Pair<String,String>, String> e : allMappings.entrySet()) {
+ String doctype = e.getKey().getFirst();
+ String from = e.getKey().getSecond();
+ String to = e.getValue();
+ builder.fieldmapping(new SchemamappingConfig.Fieldmapping.Builder().
+ chain(chain.getId().stringValue()).
+ docproc(processor.getGlobalComponentId().stringValue()).
+ indocument(from).
+ inprocessor(to).
+ doctype(doctype!=null?doctype:""));
+ }
+ allMappings.clear();
+ }
+ }
+ }
+
+ /**
+ * The field name schema map that applies to this whole chain
+ * @return doctype,from → to
+ */
+ public Map<Pair<String,String>,String> fieldNameSchemaMap() {
+ return fieldNameSchemaMap;
+ }
+
+ public static class Options {
+ // Whether or not to compress documents after processing them.
+ public final boolean compressDocuments;
+
+ public final Integer maxMessagesInQueue;
+ public final Integer maxQueueMbSize;
+ public final Integer maxQueueTimeMs;
+
+ public final Double maxConcurrentFactor;
+ public final Double documentExpansionFactor;
+ public final Integer containerCoreMemory;
+
+ public Options(boolean compressDocuments, Integer maxMessagesInQueue, Integer maxQueueMbSize, Integer maxQueueTimeMs, Double maxConcurrentFactor, Double documentExpansionFactor, Integer containerCoreMemory) {
+ this.compressDocuments = compressDocuments;
+ this.maxMessagesInQueue = maxMessagesInQueue;
+ this.maxQueueMbSize = maxQueueMbSize;
+ this.maxQueueTimeMs = maxQueueTimeMs;
+ this.maxConcurrentFactor = maxConcurrentFactor;
+ this.documentExpansionFactor = documentExpansionFactor;
+ this.containerCoreMemory = containerCoreMemory;
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChain.java b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChain.java
new file mode 100644
index 00000000000..28d0b298f64
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChain.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.docproc;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+
+import java.util.Map;
+
+import static com.yahoo.container.core.ChainsConfig.Chains.Type;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class DocprocChain extends Chain<DocumentProcessor> {
+ private Map<Pair<String, String>, String> fieldNameSchemaMap;
+ private static final Type.Enum TYPE = Type.Enum.DOCPROC;
+
+ public DocprocChain(ChainSpecification specWithoutInnerComponents, Map<Pair<String,String>, String> fieldNameSchemaMap) {
+ super(specWithoutInnerComponents);
+ this.fieldNameSchemaMap = fieldNameSchemaMap;
+ }
+
+ /**
+ * The field name schema map that applies to this whole chain
+ * @return doctype,from → to
+ */
+ public Map<Pair<String,String>,String> fieldNameSchemaMap() {
+ return fieldNameSchemaMap;
+ }
+
+ public String getServiceName() {
+ return getParent().getParent().getParent().getConfigId() + "/" + getSessionName();
+ }
+
+ public String getSessionName() {
+ return "chain." + getComponentId().stringValue();
+ }
+
+ public Type.Enum getType() {
+ return TYPE;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChains.java b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChains.java
new file mode 100644
index 00000000000..e1dbbf185c7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocprocChains.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.docproc;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.jdisc.config.SessionConfig;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.chain.Chains;
+import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
+
+/**
+ * @author einarmr
+ * @since 5.1.9
+ */
+public class DocprocChains extends Chains<DocprocChain> {
+ private final ProcessingHandler<DocprocChains> docprocHandler;
+
+ public DocprocChains(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ docprocHandler = new ProcessingHandler<>(this, "com.yahoo.docproc.jdisc.DocumentProcessingHandler");
+ addComponent(docprocHandler);
+ }
+
+ public ProcessingHandler<DocprocChains> getDocprocHandler() {
+ return docprocHandler;
+ }
+
+ private void addComponent(Component component) {
+ if (!(getParent() instanceof ContainerCluster)) {
+ return;
+ }
+ ((ContainerCluster) getParent()).addComponent(component);
+ }
+
+
+ public void addServersAndClientsForChains() {
+ if (getParent() instanceof ContainerCluster) {
+ for (DocprocChain chain: getChainGroup().getComponents())
+ addServerAndClientForChain((ContainerCluster) getParent(), chain);
+ }
+ }
+
+ private void addServerAndClientForChain(ContainerCluster cluster, DocprocChain docprocChain) {
+ docprocHandler.addServerBindings("mbus://*/" + docprocChain.getSessionName());
+
+ cluster.addMbusServer(ComponentId.fromString(docprocChain.getSessionName()));
+
+ MbusClient client = new MbusClient(docprocChain.getSessionName(), SessionConfig.Type.INTERMEDIATE);
+ client.addClientBindings("mbus://*/" + client.getSessionName());
+ addComponent(client);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocumentProcessor.java b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocumentProcessor.java
new file mode 100644
index 00000000000..789b578eb13
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/DocumentProcessor.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.docproc;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import com.yahoo.vespa.model.container.docproc.model.DocumentProcessorModel;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class DocumentProcessor extends ChainedComponent<DocumentProcessorModel> {
+
+ public static final String INDEXER = "com.yahoo.docprocs.indexing.IndexingProcessor";
+
+ private final Map<Pair<String, String>, String> fieldNameSchemaMap;
+
+ public DocumentProcessor(DocumentProcessorModel model) {
+ super(model);
+ this.fieldNameSchemaMap = model.fieldNameSchemaMap();
+ }
+
+ /**
+ * The field name schema map that applies to this docproc
+ * @return doctype,from → to
+ */
+ public Map<Pair<String,String>,String> fieldNameSchemaMap() {
+ return fieldNameSchemaMap;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/MbusClient.java b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/MbusClient.java
new file mode 100644
index 00000000000..87038115e0c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/MbusClient.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.docproc;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.jdisc.config.SessionConfig;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.Handler;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class MbusClient extends Handler<AbstractConfigProducer<?>> implements SessionConfig.Producer {
+ private static final ComponentSpecification CLASSNAME =
+ ComponentSpecification.fromString("com.yahoo.container.jdisc.messagebus.MbusClientProvider");
+
+ private final String sessionName;
+ private final SessionConfig.Type.Enum type;
+
+ public MbusClient(String sessionName, SessionConfig.Type.Enum type) {
+ super(new ComponentModel(new BundleInstantiationSpecification(createId(sessionName), CLASSNAME, null)));
+ this.sessionName = sessionName;
+ this.type = type;
+ }
+
+ private static ComponentId createId(String sessionName) {
+ return ComponentId.fromString(sessionName).nestInNamespace(
+ ComponentId.fromString("MbusClient"));
+ }
+
+ @Override
+ public void getConfig(SessionConfig.Builder sb) {
+ sb.
+ name(sessionName).
+ type(type);
+ }
+
+ public String getSessionName() {
+ return sessionName;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/model/DocumentProcessorModel.java b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/model/DocumentProcessorModel.java
new file mode 100644
index 00000000000..1f166540b94
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/docproc/model/DocumentProcessorModel.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.docproc.model;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import net.jcip.annotations.Immutable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+@Immutable
+public class DocumentProcessorModel extends ChainedComponentModel {
+ private Map<Pair<String, String>, String> fieldNameSchemaMap = new HashMap<>();
+
+ public DocumentProcessorModel(BundleInstantiationSpecification bundleInstantiationSpec, Dependencies dependencies, Map<Pair<String, String>, String> fieldNameSchemaMap) {
+ super(bundleInstantiationSpec, dependencies);
+ this.fieldNameSchemaMap.putAll(fieldNameSchemaMap);
+ }
+
+ /**
+ * The field name schema map that applies to this docproc
+ * @return doctype,from → to
+ */
+ public Map<Pair<String,String>,String> fieldNameSchemaMap() {
+ return fieldNameSchemaMap;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java
new file mode 100644
index 00000000000..24aeda9ed84
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java
@@ -0,0 +1,149 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+import org.w3c.dom.Element;
+
+import static com.yahoo.component.ComponentSpecification.fromString;
+import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.21.0
+ */
+public class ConnectorFactory extends SimpleComponent implements ConnectorConfig.Producer {
+
+ private final String name;
+ private volatile int listenPort;
+ private final Element legacyConfig;
+
+ public ConnectorFactory(final String name, final int listenPort, final Element legacyConfig) {
+ super(new ComponentModel(
+ new BundleInstantiationSpecification(new ComponentId(name),
+ fromString("com.yahoo.jdisc.http.server.jetty.ConnectorFactory"),
+ fromString("jdisc_http_service"))
+
+ ));
+
+
+ this.name = name;
+ this.listenPort = listenPort;
+ this.legacyConfig = legacyConfig;
+ }
+
+ @Override
+ public void getConfig(ConnectorConfig.Builder connectorBuilder) {
+ if (legacyConfig != null) {
+ {
+ Element tcpKeepAliveEnabled = XML.getChild(legacyConfig, "tcpKeepAliveEnabled");
+ if (tcpKeepAliveEnabled != null) {
+ connectorBuilder.tcpKeepAliveEnabled(Boolean.valueOf(XML.getValue(tcpKeepAliveEnabled).trim()));
+ }
+ }
+ {
+ Element tcpNoDelayEnabled = XML.getChild(legacyConfig, "tcpNoDelayEnabled");
+ if (tcpNoDelayEnabled != null) {
+ connectorBuilder.tcpNoDelay(Boolean.valueOf(XML.getValue(tcpNoDelayEnabled).trim()));
+ }
+ }
+ {
+ Element tcpListenBacklogLength = XML.getChild(legacyConfig, "tcpListenBacklogLength");
+ if (tcpListenBacklogLength != null) {
+ connectorBuilder.acceptQueueSize(Integer.parseInt(XML.getValue(tcpListenBacklogLength).trim()));
+ }
+ }
+ {
+ Element idleConnectionTimeout = XML.getChild(legacyConfig, "idleConnectionTimeout");
+ if (idleConnectionTimeout != null) {
+ connectorBuilder.idleTimeout(Double.parseDouble(XML.getValue(idleConnectionTimeout).trim()));
+ }
+ }
+ {
+ Element soLinger = XML.getChild(legacyConfig, "soLinger");
+ if (soLinger != null) {
+
+ connectorBuilder.soLingerTime((int) Double.parseDouble(XML.getValue(soLinger).trim()));
+ }
+ }
+ {
+ Element sendBufferSize = XML.getChild(legacyConfig, "sendBufferSize");
+ if (sendBufferSize != null) {
+ connectorBuilder.outputBufferSize(Integer.parseInt(XML.getValue(sendBufferSize).trim()));
+ }
+ }
+ {
+ Element maxHeaderSize = XML.getChild(legacyConfig, "maxHeaderSize");
+ if (maxHeaderSize != null) {
+ connectorBuilder.headerCacheSize(Integer.parseInt(XML.getValue(maxHeaderSize).trim()));
+ }
+ }
+
+ Element ssl = XML.getChild(legacyConfig, "ssl");
+ Element sslEnabled = XML.getChild(ssl, "enabled");
+ if (ssl != null &&
+ sslEnabled != null &&
+ Boolean.parseBoolean(XML.getValue(sslEnabled).trim())) {
+ ConnectorConfig.Ssl.Builder sslBuilder = new ConnectorConfig.Ssl.Builder();
+ sslBuilder.enabled(true);
+ {
+ Element keyStoreType = XML.getChild(ssl, "keyStoreType");
+ if (keyStoreType != null) {
+ sslBuilder.keyStoreType(KeyStoreType.Enum.valueOf(XML.getValue(keyStoreType).trim()));
+ }
+ }
+ {
+ Element keyStorePath = XML.getChild(ssl, "keyStorePath");
+ if (keyStorePath != null) {
+ sslBuilder.keyStorePath(XML.getValue(keyStorePath).trim());
+ }
+ }
+ {
+ Element trustStorePath = XML.getChild(ssl, "trustStorePath");
+ if (trustStorePath != null) {
+ sslBuilder.trustStorePath(XML.getValue(trustStorePath).trim());
+ }
+ }
+ {
+ Element keyDBKey = XML.getChild(ssl, "keyDBKey");
+ if (keyDBKey != null) {
+ sslBuilder.keyDbKey(XML.getValue(keyDBKey).trim());
+ }
+ }
+ {
+ Element algorithm = XML.getChild(ssl, "algorithm");
+ if (algorithm != null) {
+ sslBuilder.sslKeyManagerFactoryAlgorithm(XML.getValue(algorithm).trim());
+ }
+ }
+ {
+ Element protocol = XML.getChild(ssl, "protocol");
+ if (protocol != null) {
+ sslBuilder.protocol(XML.getValue(protocol).trim());
+ }
+ }
+ connectorBuilder.ssl(sslBuilder);
+ }
+ }
+
+ connectorBuilder.listenPort(listenPort);
+ connectorBuilder.name(name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getListenPort() {
+ return listenPort;
+ }
+
+ public void setListenPort(int httpPort) {
+ this.listenPort = httpPort;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/Filter.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Filter.java
new file mode 100644
index 00000000000..f9a416a6fd6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Filter.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class Filter extends ChainedComponent<ChainedComponentModel> {
+
+ public Filter(ChainedComponentModel model) {
+ super(model);
+ }
+
+ public FilterConfigProvider addAndInjectConfigProvider() {
+ FilterConfigProvider filterConfigProvider = new FilterConfigProvider(model);
+ addComponent(filterConfigProvider);
+ inject(filterConfigProvider);
+ return filterConfigProvider;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterChains.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterChains.java
new file mode 100644
index 00000000000..f38c99bd631
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterChains.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.component.chain.Chains;
+
+/**
+ * @author tonytv
+ */
+public class FilterChains extends Chains<Chain<Filter>> {
+
+ public FilterChains(AbstractConfigProducer parent) {
+ super(parent, "filters");
+
+ addChild(new SimpleComponent("com.yahoo.container.http.filter.FilterChainRepository"));
+ }
+
+ public boolean hasChain(ComponentId filterChain) {
+ for (Chain<Filter> chain : allChains().allComponents()) {
+ if (chain.getId().equals(filterChain))
+ return true;
+ }
+ return false;
+ }
+
+ public boolean hasChainThatInherits(ComponentId filterChain) {
+ for (Chain<Filter> chain : allChains().allComponents()) {
+ for (ComponentSpecification spec : chain.getChainSpecification().inheritance.chainSpecifications) {
+ if(spec.toId().equals(filterChain))
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterConfigProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterConfigProvider.java
new file mode 100644
index 00000000000..5c6ce3454a8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/FilterConfigProvider.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.core.http.HttpFilterConfig;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static com.yahoo.container.core.http.HttpFilterConfig.Param;
+
+/**
+ * @author gjoranv
+ * @since 5.1.23
+ */
+public class FilterConfigProvider extends SimpleComponent implements HttpFilterConfig.Producer {
+
+ private static final ComponentSpecification filterConfigProviderClass =
+ ComponentSpecification.fromString(com.yahoo.container.FilterConfigProvider.class.getName());
+
+ private final ChainedComponentModel filterModel;
+ private HashMap<String, String> configMap = new LinkedHashMap<>();
+
+ public FilterConfigProvider(ChainedComponentModel filterModel) {
+ super(new ComponentModel(
+ new BundleInstantiationSpecification(
+ configProviderId(filterModel.getComponentId()),
+ filterConfigProviderClass,
+ null)));
+
+ this.filterModel = filterModel;
+ }
+
+ @Override
+ public void getConfig(HttpFilterConfig.Builder builder) {
+ builder.filterName(filterModel.getComponentId().stringValue())
+ .filterClass(filterModel.getClassId().stringValue());
+
+ for (Map.Entry<String, String> param : configMap.entrySet()) {
+ builder.param(
+ new Param.Builder()
+ .name(param.getKey())
+ .value(param.getValue()));
+ }
+ }
+
+ public String putConfig(String key, String value) {
+ return configMap.put(key, value);
+ }
+
+ static ComponentId configProviderId(ComponentId filterId) {
+ return ComponentId.fromString("filterConfig").nestInNamespace(filterId);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/Http.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Http.java
new file mode 100644
index 00000000000..7c44ce8e44f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/Http.java
@@ -0,0 +1,123 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.jdisc.config.HttpServerConfig;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+// This can be collapsed into JettyHttpServer now
+/**
+ * @author tonytv
+ */
+public class Http extends AbstractConfigProducer<AbstractConfigProducer<?>>
+ implements HttpServerConfig.Producer, ServerConfig.Producer {
+
+ public static class Binding {
+ public final ComponentSpecification filterId;
+ public final String binding;
+
+ public Binding(ComponentSpecification filterId, String binding) {
+ this.filterId = filterId;
+ this.binding = binding;
+ }
+ }
+
+ private FilterChains filterChains;
+ private JettyHttpServer httpServer;
+ public final List<Binding> bindings;
+
+ public Http(List<Binding> bindings) {
+ super( "http");
+ this.bindings = Collections.unmodifiableList(bindings);
+ }
+
+ public void setFilterChains(FilterChains filterChains) {
+ this.filterChains = filterChains;
+ }
+
+ public FilterChains getFilterChains() {
+ return filterChains;
+ }
+
+ public JettyHttpServer getHttpServer() {
+ return httpServer;
+ }
+
+ public void setHttpServer(JettyHttpServer newServer) {
+ JettyHttpServer oldServer = this.httpServer;
+ this.httpServer = newServer;
+
+ if (oldServer == null && newServer != null) {
+ addChild(newServer);
+ } else if (newServer == null && oldServer != null) {
+ removeChild(oldServer);
+ } else if (newServer == null && oldServer == null) {
+ //do nothing
+ } else {
+ //none of them are null
+ removeChild(oldServer);
+ addChild(newServer);
+ }
+ }
+
+ public void removeAllServers() {
+ setHttpServer(null);
+ }
+
+ public List<Binding> getBindings() {
+ return bindings;
+ }
+
+ @Override
+ public void getConfig(HttpServerConfig.Builder builder) {
+ for (Binding binding: bindings)
+ builder.filter(filterBindings(binding));
+ }
+
+ @Override
+ public void getConfig(ServerConfig.Builder builder) {
+ for (final Binding binding : bindings) {
+ builder.filter(
+ new ServerConfig.Filter.Builder()
+ .id(binding.filterId.stringValue())
+ .binding(binding.binding));
+ }
+ }
+
+ static HttpServerConfig.Filter.Builder filterBindings(Binding binding) {
+ HttpServerConfig.Filter.Builder builder = new HttpServerConfig.Filter.Builder();
+ builder.id(binding.filterId.stringValue()).
+ binding(binding.binding);
+ return builder;
+ }
+
+
+ @Override
+ public void validate() throws Exception {
+ validate(bindings);
+ }
+
+ void validate(Collection<Binding> bindings) {
+ if (!bindings.isEmpty()) {
+ if (filterChains == null)
+ throw new IllegalArgumentException("Null FilterChains is not allowed when there are filter bindings!");
+
+ ComponentRegistry<ChainedComponent<?>> filters = filterChains.componentsRegistry();
+ ComponentRegistry<Chain<Filter>> chains = filterChains.allChains();
+
+ for (Binding binding: bindings) {
+ if (filters.getComponent(binding.filterId) == null && chains.getComponent(binding.filterId) == null)
+ throw new RuntimeException("Can't find filter " + binding.filterId + " for binding " + binding.binding);
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java
new file mode 100644
index 00000000000..30a9e12bf92
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.yahoo.component.ComponentSpecification.fromString;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.16.0
+ */
+public class JettyHttpServer extends SimpleComponent implements ServerConfig.Producer {
+
+ private List<ConnectorFactory> connectorFactories = new ArrayList<>();
+
+ public JettyHttpServer(ComponentId id) {
+ super(new ComponentModel(
+ new BundleInstantiationSpecification(id,
+ fromString("com.yahoo.jdisc.http.server.jetty.JettyHttpServer"),
+ fromString("jdisc_http_service"))
+ ));
+ final FilterBindingsProviderComponent filterBindingsProviderComponent = new FilterBindingsProviderComponent(id);
+ addChild(filterBindingsProviderComponent);
+ inject(filterBindingsProviderComponent);
+ }
+
+ public void addConnector(ConnectorFactory connectorFactory) {
+ connectorFactories.add(connectorFactory);
+ addChild(connectorFactory);
+ }
+
+ public void removeConnector(ConnectorFactory connectorFactory) {
+ if (connectorFactory == null) {
+ return;
+ }
+ removeChild(connectorFactory);
+ connectorFactories.remove(connectorFactory);
+ }
+
+ public List<ConnectorFactory> getConnectorFactories() {
+ return Collections.unmodifiableList(connectorFactories);
+ }
+
+ @Override
+ public void getConfig(ServerConfig.Builder builder) {
+ }
+
+ static ComponentModel providerComponentModel(final ComponentId parentId, String className) {
+ final ComponentSpecification classNameSpec = new ComponentSpecification(
+ className);
+ return new ComponentModel(new BundleInstantiationSpecification(
+ classNameSpec.nestInNamespace(parentId),
+ classNameSpec,
+ null));
+ }
+
+ public static final class FilterBindingsProviderComponent extends SimpleComponent {
+ public FilterBindingsProviderComponent(final ComponentId parentId) {
+ super(providerComponentModel(parentId, "com.yahoo.container.jdisc.FilterBindingsProvider"));
+ }
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/package-info.java
new file mode 100644
index 00000000000..304a6cd5052
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterBuilder.java
new file mode 100644
index 00000000000..9f033160f6d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterBuilder.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http.xml;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.DomComponentBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainedComponentModelBuilder;
+import com.yahoo.vespa.model.container.http.Filter;
+import com.yahoo.vespa.model.container.http.FilterConfigProvider;
+import org.w3c.dom.Element;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class FilterBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Filter> {
+
+ protected Filter doBuild(AbstractConfigProducer ancestor, Element filterElement) {
+ ChainedComponentModelBuilder modelBuilder = new ChainedComponentModelBuilder(filterElement);
+ Filter filter = new Filter(modelBuilder.build());
+ DomComponentBuilder.addChildren(ancestor, filterElement, filter);
+ addFilterConfig(filterElement, filter);
+
+ return filter;
+ }
+
+ private static void addFilterConfig(Element filterElement, Filter filter) {
+ Element filterConfigElement = XML.getChild(filterElement, "filter-config");
+ if (filterConfigElement == null)
+ return;
+
+ FilterConfigProvider filterConfigProvider = filter.addAndInjectConfigProvider();
+ putFilterConfig(filterConfigElement, filterConfigProvider);
+ }
+
+ private static void putFilterConfig(Element filterConfigElement, FilterConfigProvider filterConfigProvider) {
+ for (Element e : XML.getChildren(filterConfigElement)) {
+ filterConfigProvider.putConfig(e.getTagName(), XML.getValue(e));
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainBuilder.java
new file mode 100644
index 00000000000..65f12cf4e3f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainBuilder.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http.xml;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.http.Filter;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import static com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder.ComponentType;
+
+/**
+ * @author tonytv
+ */
+public class FilterChainBuilder extends DomChainBuilderBase<Filter, Chain<Filter>> {
+ private static Collection<ComponentType<Filter>> allowedComponentTypes = Collections.singleton(ComponentType.filter);
+
+
+ public FilterChainBuilder(Map<String, ComponentType> outerFilterTypeByComponentName) {
+ super(allowedComponentTypes, outerFilterTypeByComponentName);
+ }
+
+ @Override
+ protected Chain<Filter> buildChain(AbstractConfigProducer ancestor, Element producerSpec, ChainSpecification specWithoutInnerComponents) {
+ return new Chain<>(specWithoutInnerComponents);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainsBuilder.java
new file mode 100644
index 00000000000..df38402be91
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/FilterChainsBuilder.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http.xml;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ChainsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder.ComponentType;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainBuilderBase;
+import com.yahoo.vespa.model.builder.xml.dom.chains.DomChainsBuilder;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.http.Filter;
+import com.yahoo.vespa.model.container.http.FilterChains;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * @author tonytv
+ */
+public class FilterChainsBuilder extends DomChainsBuilder<Filter, Chain<Filter>, FilterChains> {
+ private static final Collection<ComponentType<Filter>> allowedComponentTypes =
+ Collections.singleton(ComponentType.filter);
+
+ //TODO: simplify
+ private static final Map<String, Class<? extends DomChainBuilderBase<? extends Filter, ? extends Chain<Filter>>>> chainType2BuilderClass =
+ Collections.unmodifiableMap(
+ new LinkedHashMap<String, Class<? extends DomChainBuilderBase<? extends Filter, ? extends Chain<Filter>>>>() {{
+ put("chain", FilterChainBuilder.class); // TODO: remove when 'chain' under 'http' is removed from xml schema
+ put("request-chain", FilterChainBuilder.class);
+ put("response-chain", FilterChainBuilder.class);
+ }});
+
+ public FilterChainsBuilder() {
+ super(null, allowedComponentTypes, null);
+ }
+
+ @Override
+ protected FilterChains newChainsInstance(AbstractConfigProducer parent) {
+ return new FilterChains(parent);
+ }
+
+ @Override
+ protected ChainsBuilder<Filter, Chain<Filter>> readChains(
+ AbstractConfigProducer ancestor,
+ List<Element> allChainsElems, Map<String, ComponentsBuilder.ComponentType> outerComponentTypeByComponentName) {
+
+ return new ChainsBuilder<>(ancestor, allChainsElems, outerComponentTypeByComponentName, chainType2BuilderClass);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/HttpBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/HttpBuilder.java
new file mode 100644
index 00000000000..d3ccefc3e26
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/HttpBuilder.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http.xml;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.http.FilterChains;
+import com.yahoo.vespa.model.container.http.Http;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class HttpBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Http> {
+
+ @Override
+ protected Http doBuild(AbstractConfigProducer ancestor, Element spec) {
+ FilterChains filterChains;
+ List<Http.Binding> bindings = new ArrayList<>();
+
+ Element filteringElem = XML.getChild(spec, "filtering");
+ if (filteringElem != null) {
+ filterChains = new FilterChainsBuilder().build(ancestor, filteringElem);
+ bindings = readFilterBindings(filteringElem);
+ } else {
+ filterChains = new FilterChainsBuilder().newChainsInstance(ancestor);
+ }
+
+ Http http = new Http(bindings);
+ http.setFilterChains(filterChains);
+
+ buildHttpServers(ancestor, http, spec);
+
+ return http;
+ }
+
+ private List<Http.Binding> readFilterBindings(Element filteringSpec) {
+ List<Http.Binding> result = new ArrayList<>();
+
+ for (Element child: XML.getChildren(filteringSpec)) {
+ String tagName = child.getTagName();
+ if ((tagName.equals("request-chain") || tagName.equals("response-chain"))) {
+ ComponentSpecification chainId = XmlHelper.getIdRef(child);
+
+ for (Element bindingSpec: XML.getChildren(child, "binding")) {
+ String binding = XML.getValue(bindingSpec);
+ result.add(new Http.Binding(chainId, binding));
+ }
+ }
+ }
+ return result;
+ }
+
+ private void buildHttpServers(AbstractConfigProducer ancestor, Http http, Element spec) {
+ http.setHttpServer(new JettyHttpServerBuilder().build(ancestor, spec));
+ }
+
+ static int readPort(Element spec, DeployState deployState) {
+ String portString = spec.getAttribute("port");
+
+ int port = Integer.parseInt(portString);
+ if (port < 0)
+ throw new IllegalArgumentException(String.format("Invalid port %d.", port));
+
+ int legalPortInHostedVespa = Container.BASEPORT;
+ if (deployState.isHostedVespa() && port != legalPortInHostedVespa) {
+ deployState.getDeployLogger().log(LogLevel.WARNING,
+ String.format("Trying to set port to %d for http server with id %s. You cannot set port to anything else than %s",
+ port, spec.getAttribute("id"), legalPortInHostedVespa));
+ }
+
+ return port;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java
new file mode 100644
index 00000000000..be92c00652f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyConnectorBuilder.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http.xml;
+
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.http.ConnectorFactory;
+import org.w3c.dom.Element;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.21.0
+ */
+public class JettyConnectorBuilder extends VespaDomBuilder.DomConfigProducerBuilder<ConnectorFactory> {
+ @Override
+ protected ConnectorFactory doBuild(AbstractConfigProducer ancestor, Element serverSpec) {
+ String name = XmlHelper.getIdString(serverSpec);
+ int port = HttpBuilder.readPort(serverSpec, ancestor.getRoot().getDeployState());
+
+ Element legacyServerConfig = XML.getChild(serverSpec, "config");
+ if (legacyServerConfig != null) {
+ String configName = legacyServerConfig.getAttribute("name");
+ if (!configName.equals("container.jdisc.config.http-server")) {
+ legacyServerConfig = null;
+ }
+ }
+ return new ConnectorFactory(name, port, legacyServerConfig);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java
new file mode 100644
index 00000000000..c28849dba3f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http.xml;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.http.ConnectorFactory;
+import com.yahoo.vespa.model.container.http.JettyHttpServer;
+import org.w3c.dom.Element;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.17.0
+ */
+public class JettyHttpServerBuilder extends VespaDomBuilder.DomConfigProducerBuilder<JettyHttpServer> {
+ @Override
+ protected JettyHttpServer doBuild(AbstractConfigProducer ancestor, Element http) {
+ JettyHttpServer jettyHttpServer = new JettyHttpServer(new ComponentId("jdisc-jetty"));
+ for (Element serverSpec: XML.getChildren(http, "server")) {
+ ConnectorFactory connectorFactory = new JettyConnectorBuilder().build(ancestor, serverSpec);
+ jettyHttpServer.addConnector(connectorFactory);
+ }
+
+ return jettyHttpServer;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/Jersey2Servlet.java b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/Jersey2Servlet.java
new file mode 100644
index 00000000000..def9442f9e4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/Jersey2Servlet.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.jersey;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.VersionSpecification;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.component.Servlet;
+
+/**
+ * @author tonytv
+ */
+public class Jersey2Servlet extends Servlet {
+ public static final String BUNDLE = "container-jersey2";
+ public static final String CLASS = "com.yahoo.container.servlet.jersey.JerseyServletProvider";
+
+ private static final ComponentId REST_API_NAMESPACE = ComponentId.fromString("rest-api");
+
+ public Jersey2Servlet(String bindingPath) {
+ super(new ComponentModel(
+ new BundleInstantiationSpecification(idSpecFromPath(bindingPath),
+ ComponentSpecification.fromString(CLASS),
+ ComponentSpecification.fromString(BUNDLE))),
+ bindingPath + "/*");
+ }
+
+ private static ComponentSpecification idSpecFromPath(String path) {
+ return new ComponentSpecification(
+ RestApi.idFromPath(path),
+ VersionSpecification.emptyVersionSpecification,
+ REST_API_NAMESPACE);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/JerseyHandler.java b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/JerseyHandler.java
new file mode 100644
index 00000000000..3c7c9dde86a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/JerseyHandler.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.jersey;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.component.Handler;
+
+/**
+ * @author gjoranv
+ * @since 5.6
+ */
+public class JerseyHandler extends Handler<AbstractConfigProducer<?>> {
+
+ public static final String BUNDLE = "container-jersey";
+ public static final String CLASS = "com.yahoo.container.jdisc.jersey.JerseyHandler";
+
+ public JerseyHandler(String bindingPath) {
+ super(new ComponentModel(bundleSpec(CLASS, BUNDLE, bindingPath)));
+ }
+
+ public static BundleInstantiationSpecification bundleSpec(String className, String bundle, String bindingPath) {
+ return BundleInstantiationSpecification.getFromStrings(
+ className + "-" + RestApi.idFromPath(bindingPath),
+ className,
+ bundle);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApi.java b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApi.java
new file mode 100644
index 00000000000..f546e43318f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApi.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.jersey;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.config.jersey.JerseyInitConfig;
+import com.yahoo.vespa.model.container.component.Component;
+
+import java.util.Optional;
+
+/**
+ * @author gjoranv
+ * @since 5.6
+ */
+public class RestApi extends AbstractConfigProducer<AbstractConfigProducer<?>> implements
+ JerseyInitConfig.Producer
+{
+ public final boolean isJersey2;
+ private final String bindingPath;
+ private final Component<?, ?> jerseyHandler;
+ private RestApiContext restApiContext;
+
+ public RestApi(String bindingPath, boolean isJersey2) {
+ super(idFromPath(bindingPath));
+ this.bindingPath = bindingPath;
+ this.isJersey2 = isJersey2;
+
+ jerseyHandler = isJersey2 ?
+ createJersey2Servlet(this.bindingPath):
+ createJersey1Handler(this.bindingPath);
+ addChild(jerseyHandler);
+ }
+
+ public static String idFromPath(String path) {
+ return path.replaceAll("/", "|");
+ }
+
+ private Jersey2Servlet createJersey2Servlet(String bindingPath) {
+ return new Jersey2Servlet(bindingPath);
+ }
+
+ private static JerseyHandler createJersey1Handler(String bindingPath) {
+ JerseyHandler jerseyHandler = new JerseyHandler(bindingPath);
+ jerseyHandler.addServerBindings(getBindings(bindingPath));
+ return jerseyHandler;
+ }
+
+ public String getBindingPath() {
+ return bindingPath;
+ }
+
+ @Override
+ public void getConfig(JerseyInitConfig.Builder builder) {
+ builder.jerseyMapping(bindingPath);
+ }
+
+ public void setRestApiContext(RestApiContext restApiContext) {
+ this.restApiContext = restApiContext;
+ addChild(restApiContext);
+ jerseyHandler.inject(restApiContext);
+ }
+
+ public RestApiContext getContext() { return restApiContext; }
+
+ public Optional<JerseyHandler> getJersey1Handler() {
+ return isJersey2 ?
+ Optional.empty():
+ Optional.of((JerseyHandler)jerseyHandler);
+ }
+
+ public Optional<Jersey2Servlet> getJersey2Servlet() {
+ return isJersey2 ?
+ Optional.of((Jersey2Servlet)jerseyHandler) :
+ Optional.empty();
+ }
+
+ private static String[] getBindings(String bindingPath) {
+ String bindingWithoutScheme = "://*/" + bindingPath + "/*";
+ return new String[] {"http" + bindingWithoutScheme, "https" + bindingWithoutScheme};
+ }
+
+ public void prepare() {
+ restApiContext.prepare();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApiContext.java b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApiContext.java
new file mode 100644
index 00000000000..e18a40e1c6a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/RestApiContext.java
@@ -0,0 +1,149 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.jersey;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.di.config.JerseyBundlesConfig;
+import com.yahoo.container.di.config.JerseyInjectionConfig;
+import com.yahoo.container.di.config.JerseyInjectionConfig.Inject;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.SimpleComponent;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+
+/**
+ * @author gjoranv
+ * @since 5.16
+ */
+public class RestApiContext extends SimpleComponent implements
+ JerseyBundlesConfig.Producer,
+ JerseyInjectionConfig.Producer
+{
+ private static final Logger log = Logger.getLogger(RestApi.class.getName());
+ public static final String CONTAINER_CLASS = "com.yahoo.container.di.config.RestApiContext";
+
+ private final List<BundleInfo> bundles = new ArrayList<>();
+
+ // class name -> componentId
+ private final Map<String, String> injectComponentForClass = new LinkedHashMap<>();
+
+ private final String bindingPath;
+
+ @Nullable
+ private ContainerCluster containerCluster;
+
+ public RestApiContext(AbstractConfigProducer<?> ancestor, String bindingPath) {
+ super(componentModel(bindingPath));
+ this.bindingPath = bindingPath;
+
+ if (ancestor instanceof ContainerCluster)
+ containerCluster = (ContainerCluster)ancestor;
+
+ }
+
+ private static ComponentModel componentModel(String bindingPath) {
+ return new ComponentModel(BundleInstantiationSpecification.getFromStrings(
+ CONTAINER_CLASS + "-" + RestApi.idFromPath(bindingPath),
+ CONTAINER_CLASS,
+ null));
+ }
+
+ @Override
+ public void getConfig(JerseyBundlesConfig.Builder builder) {
+ builder.bundles(createBundlesConfig(bundles));
+ }
+
+ private List<JerseyBundlesConfig.Bundles.Builder> createBundlesConfig(List<BundleInfo> bundles) {
+ List<JerseyBundlesConfig.Bundles.Builder> builders = new ArrayList<>();
+ for (BundleInfo b : bundles) {
+ builders.add(
+ new JerseyBundlesConfig.Bundles.Builder()
+ .spec(b.spec)
+ .packages(b.getPackagesToScan())
+ );
+ }
+ return builders;
+ }
+
+ public void addBundles(Collection<BundleInfo> newBundles) {
+ bundles.addAll(newBundles);
+ }
+
+ @Override
+ public void getConfig(JerseyInjectionConfig.Builder builder) {
+ for (Map.Entry<String, String> i : injectComponentForClass.entrySet()) {
+ builder.inject(new Inject.Builder()
+ .forClass(i.getKey())
+ .instance(i.getValue()));
+ }
+ }
+
+ public void addInjections(Map<String, String> injections) {
+ injectComponentForClass.putAll(injections);
+ }
+
+ @Override
+ public void validate() throws Exception {
+ super.validate();
+
+ if (bundles.isEmpty())
+ log.warning("No bundles in rest-api '" + bindingPath +
+ "' - components will only be loaded from classpath.");
+ }
+
+ public void prepare() {
+ if (containerCluster == null) return;
+
+ containerCluster.getAllComponents().stream().
+ filter(isCycleGeneratingComponent.negate()).
+ forEach(this::inject);
+ }
+
+
+ /*
+ * Example problem
+ *
+ * RestApiContext -> ApplicationStatusHandler -> ComponentRegistry<HttpServer> -> JettyHttpServer -> ComponentRegistry<Jersey2Servlet> -> RestApiContext
+ */
+ private Predicate<Component> isCycleGeneratingComponent = component -> {
+ switch (component.getClassId().getName()) {
+ case CONTAINER_CLASS:
+ case JerseyHandler.CLASS:
+ case Jersey2Servlet.CLASS:
+ case "com.yahoo.jdisc.http.server.jetty.JettyHttpServer":
+ case "com.yahoo.container.handler.observability.ApplicationStatusHandler":
+ return true;
+ default:
+ return false;
+ }
+ };
+
+ public static class BundleInfo {
+ // SymbolicName[:Version]
+ public final String spec;
+
+ private final List<String> packagesToScan = new ArrayList<>();
+
+ public BundleInfo(String spec) {
+ this.spec = spec;
+ }
+
+ public List<String> getPackagesToScan() {
+ return packagesToScan;
+ }
+
+ public void addPackageToScan(String pkg) {
+ packagesToScan.add(pkg);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/xml/RestApiBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/xml/RestApiBuilder.java
new file mode 100644
index 00000000000..ed7239e0bbf
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/jersey/xml/RestApiBuilder.java
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.jersey.xml;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.jersey.RestApi;
+import com.yahoo.vespa.model.container.jersey.RestApiContext;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.yahoo.config.model.builder.xml.XmlHelper.getOptionalAttribute;
+
+/**
+ * @author gjoranv
+ * @since 5.6
+ */
+public class RestApiBuilder extends VespaDomBuilder.DomConfigProducerBuilder<RestApi> {
+
+ @Override
+ protected RestApi doBuild(AbstractConfigProducer ancestor, Element spec) {
+ String bindingPath = spec.getAttribute("path");
+ boolean jersey2 = Boolean.parseBoolean(getOptionalAttribute(spec, "jersey2").orElse("false"));
+ RestApi restApi = new RestApi(bindingPath, jersey2);
+
+ restApi.setRestApiContext(
+ createRestApiContext(ancestor, spec, bindingPath));
+ return restApi;
+ }
+
+ private RestApiContext createRestApiContext(AbstractConfigProducer ancestor, Element spec, String bindingPath) {
+ RestApiContext restApiContext = new RestApiContext(ancestor, bindingPath);
+
+ restApiContext.addBundles(getBundles(spec));
+
+ return restApiContext;
+ }
+
+ private List<RestApiContext.BundleInfo> getBundles(Element spec) {
+ List<RestApiContext.BundleInfo> bundles = new ArrayList<>();
+ for (Element bundleElement : XML.getChildren(spec, "components")) {
+ bundles.add(getBundle(bundleElement));
+ }
+ return bundles;
+ }
+
+ private RestApiContext.BundleInfo getBundle(Element bundleElement) {
+ RestApiContext.BundleInfo bundle = new RestApiContext.BundleInfo(
+ bundleElement.getAttribute("bundle"));
+
+ for (Element packageElement : XML.getChildren(bundleElement, "package"))
+ bundle.addPackageToScan(XML.getValue(packageElement));
+
+ return bundle;
+ }
+
+ // TODO: use for naming injected components instead
+ private Map<String, String> getInjections(Element spec) {
+ Map<String, String> injectForClass = new LinkedHashMap<>();
+ for (Element injectElement : XML.getChildren(spec, "inject")) {
+ injectForClass.put(injectElement.getAttribute("for-class"),
+ injectElement.getAttribute("component"));
+ }
+ return injectForClass;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/container/package-info.java
new file mode 100644
index 00000000000..74acf6dcc7f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.container;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChain.java b/config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChain.java
new file mode 100644
index 00000000000..ae7bcb64f66
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChain.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.processing;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+
+/**
+ * Represents a processing chain in the config model
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.6
+ */
+public class ProcessingChain extends Chain<Processor> {
+
+ public ProcessingChain(ChainSpecification specWithoutInnerProcessors) {
+ super(specWithoutInnerProcessors);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChains.java b/config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChains.java
new file mode 100644
index 00000000000..032aacccca8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/processing/ProcessingChains.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.processing;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.chain.Chains;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Root config producer for processing
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.6
+ */
+public class ProcessingChains extends Chains<ProcessingChain> {
+ public static final String[] defaultBindings = new String[]
+ {"http://*/processing/*", "https://*/processing/*"};
+
+
+ public ProcessingChains(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/processing/Processor.java b/config-model/src/main/java/com/yahoo/vespa/model/container/processing/Processor.java
new file mode 100644
index 00000000000..b16243576f9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/processing/Processor.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.processing;
+
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+
+/**
+ * Representation of a Processor in the configuration model
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.6
+ */
+public class Processor extends ChainedComponent<ChainedComponentModel> {
+
+ public Processor(ChainedComponentModel model) {
+ super(model);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerHttpGateway.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerHttpGateway.java
new file mode 100644
index 00000000000..88d4a0c8599
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerHttpGateway.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+
+/**
+ * @author gjoranv
+ */
+public class ContainerHttpGateway extends Container {
+
+ public ContainerHttpGateway(ContainerCluster parent, String name, int wantedPort) {
+ super(parent, name);
+
+ // TODO: when this class is removed, all ports for the gateway will map to standard container ports
+ // this is just a tjuvtriks to keep the old gateway port allocation for now.
+ setBasePort(wantedPort);
+ }
+
+ @Override
+ public String getServiceType() { return "container-httpgateway"; }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerSearch.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerSearch.java
new file mode 100644
index 00000000000..0caa84390f8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/ContainerSearch.java
@@ -0,0 +1,204 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.binaryprefix.BinaryPrefix;
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.prelude.fastsearch.FS4ResourcePool;
+import com.yahoo.prelude.semantics.SemanticRulesConfig;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.ContainerSubsystem;
+import com.yahoo.vespa.model.container.search.searchchain.HttpProvider;
+import com.yahoo.vespa.model.container.search.searchchain.LocalProvider;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import com.yahoo.vespa.model.search.*;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.search.config.QrStartConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import com.yahoo.container.QrSearchersConfig;
+import com.yahoo.search.query.profile.config.QueryProfilesConfig;
+import com.yahoo.search.pagetemplates.PageTemplatesConfig;
+
+import java.util.*;
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ * @since 5.1.9
+ */
+public class ContainerSearch extends ContainerSubsystem<SearchChains>
+ implements
+ IndexInfoConfig.Producer,
+ IlscriptsConfig.Producer,
+ QrSearchersConfig.Producer,
+ QrStartConfig.Producer,
+ QueryProfilesConfig.Producer,
+ SemanticRulesConfig.Producer,
+ PageTemplatesConfig.Producer
+{
+ private final List<AbstractSearchCluster> systems = new LinkedList<>();
+ private Options options = null;
+
+ //For legacy qrs clusters only.
+ private BinaryScaledAmount totalCacheSize = new BinaryScaledAmount();
+
+ private QueryProfiles queryProfiles;
+ private SemanticRules semanticRules;
+ private PageTemplates pageTemplates;
+ private boolean hostedVespa = false;
+
+ public ContainerSearch(ContainerCluster cluster, SearchChains chains, Options options) {
+ super(chains);
+ this.options = options;
+ this.hostedVespa = cluster.isHostedVespa();
+ cluster.addComponent(getFS4ResourcePool());
+ }
+
+ private Component<?, ComponentModel> getFS4ResourcePool() {
+ BundleInstantiationSpecification spec = BundleInstantiationSpecification.
+ getInternalSearcherSpecificationFromStrings(FS4ResourcePool.class.getName(), null);
+ return new Component<>(new ComponentModel(spec));
+ }
+
+ public void connectSearchClusters(Map<String, AbstractSearchCluster> searchClusters) {
+ systems.addAll(searchClusters.values());
+ initializeSearchChains(searchClusters);
+ }
+
+ // public for testing
+ public void initializeSearchChains(Map<String, ? extends AbstractSearchCluster> searchClusters) {
+ getChains().initialize(searchClusters, totalCacheSize);
+
+ QrsCache defaultCacheOptions = getOptions().cacheSettings.get("");
+ if (defaultCacheOptions != null) {
+ for (LocalProvider localProvider: getChains().localProviders()) {
+ localProvider.setCacheSize(defaultCacheOptions.size);
+ }
+ }
+
+ for (LocalProvider localProvider: getChains().localProviders()) {
+ QrsCache cacheOptions = getOptions().cacheSettings.get(localProvider.getClusterName());
+ if (cacheOptions != null) {
+ localProvider.setCacheSize(cacheOptions.size);
+ }
+ }
+ }
+
+ public void setTotalCacheSize(BinaryScaledAmount totalCacheSize) {
+ this.totalCacheSize = totalCacheSize;
+ }
+
+ public void setQueryProfiles(QueryProfiles queryProfiles) {
+ this.queryProfiles = queryProfiles;
+ }
+
+ public void setSemanticRules(SemanticRules semanticRules) {
+ this.semanticRules = semanticRules;
+ }
+
+ public void setPageTemplates(PageTemplates pageTemplates) {
+ this.pageTemplates = pageTemplates;
+ }
+
+ @Override
+ public void getConfig(QueryProfilesConfig.Builder builder) {
+ if (queryProfiles!=null) queryProfiles.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(SemanticRulesConfig.Builder builder) {
+ if (semanticRules!=null) semanticRules.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(PageTemplatesConfig.Builder builder) {
+ if (pageTemplates!=null) pageTemplates.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(QrStartConfig.Builder qsB) {
+ final QrStartConfig.Jvm.Builder internalBuilder = new QrStartConfig.Jvm.Builder();
+ if (hostedVespa) {
+ internalBuilder.heapSizeAsPercentageOfPhysicalMemory(33);
+ }
+ qsB.jvm(internalBuilder.directMemorySizeCache(totalCacheSizeMb()));
+ }
+
+ private int totalCacheSizeMb() {
+ if (!totalCacheSize.equals(new BinaryScaledAmount())) {
+ return (int) totalCacheSize.as(BinaryPrefix.mega);
+ } else {
+ return totalHttpProviderCacheSize();
+ }
+ }
+
+ private int totalHttpProviderCacheSize() {
+ int totalCacheSizeMb = 0;
+ for (HttpProvider provider: getChains().httpProviders())
+ totalCacheSizeMb += provider.cacheSizeMB();
+
+ return totalCacheSizeMb;
+ }
+
+ @Override
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ for (AbstractSearchCluster sc : systems) {
+ sc.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(IlscriptsConfig.Builder builder) {
+ for (AbstractSearchCluster sc : systems) {
+ sc.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(QrSearchersConfig.Builder builder) {
+ for (int i = 0; i < systems.size(); i++) {
+ AbstractSearchCluster sys = findClusterWithId(systems, i);
+ QrSearchersConfig.Searchcluster.Builder scB = new QrSearchersConfig.Searchcluster.Builder().
+ name(sys.getClusterName());
+ for (AbstractSearchCluster.SearchDefinitionSpec spec : sys.getLocalSDS()) {
+ scB.searchdef(spec.getSearchDefinition().getName());
+ }
+ scB.rankprofiles(new QrSearchersConfig.Searchcluster.Rankprofiles.Builder().configid(sys.getConfigId()));
+ scB.indexingmode(QrSearchersConfig.Searchcluster.Indexingmode.Enum.valueOf(sys.getIndexingModeName()));
+ if (sys instanceof IndexedSearchCluster) {
+ scB.rowbits(sys.getRowBits());
+ for (Dispatch tld: ((IndexedSearchCluster)sys).getTLDs()) {
+ scB.dispatcher(new QrSearchersConfig.Searchcluster.Dispatcher.Builder().
+ host(tld.getHostname()).
+ port(tld.getDispatchPort()));
+ }
+ } else {
+ scB.storagecluster(new QrSearchersConfig.Searchcluster.Storagecluster.Builder().
+ routespec(((StreamingSearchCluster)sys).getStorageRouteSpec()));
+ }
+ builder.searchcluster(scB);
+ }
+ }
+
+ private static AbstractSearchCluster findClusterWithId(List<AbstractSearchCluster> clusters, int index) {
+ for (AbstractSearchCluster sys : clusters) {
+ if (sys.getClusterIndex() == index) {
+ return sys;
+ }
+ }
+ throw new IllegalArgumentException("No search cluster with index " + index + " exists");
+ }
+
+ public Options getOptions() {
+ return options;
+ }
+
+ /**
+ * Struct that encapsulates qrserver options.
+ */
+ public static class Options {
+ public Map<String, QrsCache> cacheSettings = new LinkedHashMap<>();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/DeclaredQueryProfileVariants.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/DeclaredQueryProfileVariants.java
new file mode 100644
index 00000000000..11903b55d67
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/DeclaredQueryProfileVariants.java
@@ -0,0 +1,138 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.search.query.profile.OverridableQueryProfile;
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileVariants;
+
+import java.util.*;
+
+/**
+ * Represents a set of query profile variants (more or less) as they were declared -
+ * a helper to produce config, which is also using the "declared" representation
+ *
+ * @author bratseth
+ */
+public class DeclaredQueryProfileVariants {
+
+ private Map<String, VariantQueryProfile> variantQueryProfiles =new LinkedHashMap<>();
+
+ public DeclaredQueryProfileVariants(QueryProfile profile) {
+ // Recreates the declared view (settings per set of dimensions)
+ // from the runtime view (dimension-value pairs per variable)
+ // yes, this is a little backwards, but the complexity of two representations
+ // is contained right here...
+ // TODO: This has become unnecessary as the variants now retains the original structure
+ for (Map.Entry<String,QueryProfileVariants.FieldValues> fieldValueEntry : profile.getVariants().getFieldValues().entrySet()) {
+ for (QueryProfileVariants.FieldValue fieldValue : fieldValueEntry.getValue().asList()) {
+ addVariant(fieldValueEntry.getKey(),fieldValue.getValue(),fieldValue.getDimensionValues().getValues());
+ }
+ }
+
+ for (QueryProfileVariants.FieldValue fieldValue : profile.getVariants().getInherited().asList()) {
+ for (QueryProfile inherited : (List<QueryProfile>)fieldValue.getValue())
+ addVariantInherited(inherited,fieldValue.getDimensionValues().getValues());
+ }
+
+ dereferenceCompoundedVariants(profile,"");
+ }
+
+ private void addVariant(String name,Object value,String[] dimensionValues) {
+ String dimensionString=toCanonicalString(dimensionValues);
+ VariantQueryProfile variant=variantQueryProfiles.get(dimensionString);
+ if (variant==null) {
+ variant=new VariantQueryProfile(dimensionValues);
+ variantQueryProfiles.put(dimensionString,variant);
+ }
+ variant.getValues().put(name,value);
+ }
+
+ private void addVariantInherited(QueryProfile inherited,String[] dimensionValues) {
+ String dimensionString=toCanonicalString(dimensionValues);
+ VariantQueryProfile variant=variantQueryProfiles.get(dimensionString);
+ if (variant==null) {
+ variant=new VariantQueryProfile(dimensionValues);
+ variantQueryProfiles.put(dimensionString,variant);
+ }
+ variant.inherit(inherited);
+ }
+
+ private void dereferenceCompoundedVariants(QueryProfile profile,String prefix) {
+ // A variant of a.b is represented as the value a pointing to an anonymous profile a
+ // having the variants
+ for (Map.Entry<String,Object> entry : profile.declaredContent().entrySet()) {
+ if ( ! (entry.getValue() instanceof QueryProfile)) continue;
+ QueryProfile subProfile=(QueryProfile)entry.getValue();
+ // Export if defined implicitly in this, or if this contains overrides
+ if (!subProfile.isExplicit() || subProfile instanceof OverridableQueryProfile) {
+ String entryPrefix=prefix + entry.getKey() + ".";
+ dereferenceCompoundedVariants(subProfile.getVariants(),entryPrefix);
+ dereferenceCompoundedVariants(subProfile,entryPrefix);
+ }
+ }
+
+ if (profile.getVariants()==null) return;
+ // We need to do the same dereferencing to overridables pointed to by variants of this
+ for (Map.Entry<String,QueryProfileVariants.FieldValues> fieldValueEntry : profile.getVariants().getFieldValues().entrySet()) {
+ for (QueryProfileVariants.FieldValue fieldValue : fieldValueEntry.getValue().asList()) {
+ if ( ! (fieldValue.getValue() instanceof QueryProfile)) continue;
+ QueryProfile subProfile=(QueryProfile)fieldValue.getValue();
+ // Export if defined implicitly in this, or if this contains overrides
+ if (!subProfile.isExplicit() || subProfile instanceof OverridableQueryProfile) {
+ String entryPrefix=prefix + fieldValueEntry.getKey() + ".";
+ dereferenceCompoundedVariants(subProfile.getVariants(),entryPrefix);
+ dereferenceCompoundedVariants(subProfile,entryPrefix);
+ }
+ }
+ }
+ }
+
+ private void dereferenceCompoundedVariants(QueryProfileVariants profileVariants,String prefix) {
+ if (profileVariants==null) return;
+ for (Map.Entry<String,QueryProfileVariants.FieldValues> fieldVariant : profileVariants.getFieldValues().entrySet()) {
+ for (QueryProfileVariants.FieldValue variantValue : fieldVariant.getValue().asList()) {
+ addVariant(prefix + fieldVariant.getKey(),variantValue.getValue(),variantValue.getDimensionValues().getValues());
+ }
+ }
+ }
+
+ public String toCanonicalString(String[] dimensionValues) {
+ StringBuilder b=new StringBuilder();
+ for (String dimensionValue : dimensionValues) {
+ if (dimensionValue!=null)
+ b.append(dimensionValue);
+ else
+ b.append("*");
+ b.append(",");
+ }
+ b.deleteCharAt(b.length()-1); // Remove last,
+ return b.toString();
+ }
+
+ public Map<String, VariantQueryProfile> getVariantQueryProfiles() { return variantQueryProfiles; }
+
+ public class VariantQueryProfile {
+
+ private Map<String,Object> values=new LinkedHashMap<>();
+
+ private List<QueryProfile> inherited=new ArrayList<>();
+
+ private String[] dimensionValues;
+
+ public VariantQueryProfile(String[] dimensionValues) {
+ this.dimensionValues=dimensionValues;
+ }
+
+ public String[] getDimensionValues() { return dimensionValues; }
+
+ public void inherit(QueryProfile inheritedProfile) {
+ inherited.add(inheritedProfile);
+ }
+
+ public List<QueryProfile> inherited() { return inherited; }
+
+ public Map<String,Object> getValues() { return values; }
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/PageTemplates.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/PageTemplates.java
new file mode 100644
index 00000000000..c38001d4a83
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/PageTemplates.java
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.search.pagetemplates.config.PageTemplateXMLReader;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.search.pagetemplates.PageTemplatesConfig;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Owns the page templates to be handed to the qrs nodes.
+ * Owned by a container cluster.
+ *
+ * @author bratseth
+ */
+public class PageTemplates implements Serializable, PageTemplatesConfig.Producer {
+
+ private List<String> pages = new ArrayList<>();
+
+ /** The number of pages in this, for reporting */
+ //private int pages=0;
+
+ /** Validates page templates in an application package. The passed readers will be closed. */
+ public static void validate(ApplicationPackage applicationPackage) {
+ List<NamedReader> pageTemplateFiles=null;
+ try {
+ pageTemplateFiles=applicationPackage.getPageTemplateFiles();
+ new PageTemplateXMLReader().read(pageTemplateFiles,true); // Parse XML for validation only
+ }
+ finally {
+ NamedReader.closeAll(pageTemplateFiles);
+ }
+ }
+
+ /** Creates from an application package. The passed readers will be closed. */
+ public static PageTemplates create(ApplicationPackage applicationPackage) {
+ List<NamedReader> pageTemplateFiles=null;
+ try {
+ pageTemplateFiles=applicationPackage.getPageTemplateFiles();
+ return new PageTemplates(pageTemplateFiles);
+ }
+ finally {
+ NamedReader.closeAll(pageTemplateFiles);
+ }
+ }
+
+ // We are representing these as XML rather than a structured config type because the structure
+ // is not easily representable by config (arbitrary nesting of many types of elements within each other)
+ // and config<->xml generation will not pull its weight in work and possible bugs.
+ // The XML content is already validated when we get here.
+ public PageTemplates(List<NamedReader> readers) {
+ for (NamedReader pageReader : readers) {
+ try {
+ pages.add(contentAsString(pageReader));
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Could not read page template '" + pageReader.getName() + "'",e);
+ }
+ }
+ }
+
+ @Override
+ public void getConfig(PageTemplatesConfig.Builder builder) {
+ for (String page : pages) {
+ builder.page(page);
+ }
+ }
+
+ private String contentAsString(Reader pageReader) throws IOException {
+ BufferedReader bufferedReader=new BufferedReader(pageReader);
+ StringBuilder b=new StringBuilder();
+ String line;
+ while (null!=(line=bufferedReader.readLine())) {
+ b.append(line);
+ b.append("\n");
+ }
+ return b.toString();
+ }
+
+ @Override
+ public String toString() {
+ return pages.toString();
+ }
+
+ /**
+ * The config produced by this
+ *
+ * @return page templates config
+ */
+ public PageTemplatesConfig getConfig() {
+ PageTemplatesConfig.Builder ptB = new PageTemplatesConfig.Builder();
+ getConfig(ptB);
+ return new PageTemplatesConfig(ptB);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/QrsCache.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/QrsCache.java
new file mode 100644
index 00000000000..72dc2379dbf
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/QrsCache.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+/**
+ * A helper class to wrap a set of QRS cache settings.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class QrsCache {
+ public final Integer size;
+
+ public QrsCache(Integer size) {
+ this.size = size;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfiles.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfiles.java
new file mode 100644
index 00000000000..cc57a58ed47
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfiles.java
@@ -0,0 +1,276 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.search.query.profile.BackedOverridableQueryProfile;
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.SubstituteString;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.config.QueryProfilesConfig;
+
+import java.io.Serializable;
+import java.util.*;
+import java.util.Map.Entry;
+
+/**
+ * Owns the query profiles and query profile types to be handed to the qrs nodes.
+ * Owned by a container cluster
+ *
+ * @author bratseth
+ */
+public class QueryProfiles implements Serializable, QueryProfilesConfig.Producer {
+
+ private QueryProfileRegistry registry;
+
+ /**
+ * Creates a new set of query profiles for which the config can be returned at request
+ *
+ * @param registry the registry containing the query profiles and types of this.
+ * The given registry cannot be frozen on calling this.
+ */
+ public QueryProfiles(QueryProfileRegistry registry) {
+ this.registry = registry;
+ }
+
+ public QueryProfiles() {
+ this.registry = new QueryProfileRegistry();
+ }
+
+ public QueryProfileRegistry getRegistry() {
+ return registry;
+ }
+
+ @Override
+ public void getConfig(QueryProfilesConfig.Builder builder) {
+ for (QueryProfile profile : registry.allComponents()) {
+ builder.queryprofile(createConfig(profile));
+ }
+ for (QueryProfileType profileType : registry.getTypeRegistry().allComponents()) {
+ if ( ! profileType.isBuiltin())
+ builder.queryprofiletype(createConfig(profileType));
+ }
+ }
+
+ private QueryProfilesConfig.Queryprofile.Builder createConfig(QueryProfile profile) {
+ QueryProfilesConfig.Queryprofile.Builder qB = new QueryProfilesConfig.Queryprofile.Builder();
+ qB.id(profile.getId().stringValue());
+ if (profile.getType() != null)
+ qB.type(profile.getType().getId().stringValue());
+ for (QueryProfile inherited : profile.inherited())
+ qB.inherit(inherited.getId().stringValue());
+
+ if (profile.getVariants()!=null) {
+ for (String dimension : profile.getVariants().getDimensions())
+ qB.dimensions(dimension);
+ }
+ addFieldChildren(qB, profile, "");
+ addVariants(qB, profile);
+ return qB;
+ }
+
+ private void addFieldChildren(QueryProfilesConfig.Queryprofile.Builder qpB, QueryProfile profile, String namePrefix) {
+ List<Map.Entry<String,Object>> content=new ArrayList<>(profile.declaredContent().entrySet());
+ Collections.sort(content,new MapEntryKeyComparator());
+ if (profile.getValue()!=null) { // Add "prefix with dot removed"=value:
+ QueryProfilesConfig.Queryprofile.Property.Builder propB = new QueryProfilesConfig.Queryprofile.Property.Builder();
+ String fullName = namePrefix.substring(0, namePrefix.length() - 1);
+ Object value = profile.getValue();
+ if (value instanceof SubstituteString)
+ value=value.toString(); // Send only types understood by configBuilder downwards
+ propB.name(fullName);
+ if (value!=null) propB.value(value.toString());
+ qpB.property(propB);
+ }
+ for (Map.Entry<String,Object> field : content) {
+ addField(qpB, profile, field, namePrefix);
+ }
+ }
+
+ private void addVariantFieldChildren(QueryProfilesConfig.Queryprofile.Queryprofilevariant.Builder qpB,
+ QueryProfile profile, String namePrefix) {
+ List<Map.Entry<String,Object>> content=new ArrayList<>(profile.declaredContent().entrySet());
+ Collections.sort(content,new MapEntryKeyComparator());
+ if (profile.getValue()!=null) { // Add "prefix with dot removed"=value:
+ QueryProfilesConfig.Queryprofile.Queryprofilevariant.Property.Builder propB = new QueryProfilesConfig.Queryprofile.Queryprofilevariant.Property.Builder();
+ String fullName = namePrefix.substring(0, namePrefix.length() - 1);
+ Object value = profile.getValue();
+ if (value instanceof SubstituteString)
+ value=value.toString(); // Send only types understood by configBuilder downwards
+ propB.name(fullName);
+ if (value!=null) propB.value(value.toString());
+ qpB.property(propB);
+ }
+ for (Map.Entry<String,Object> field : content) {
+ addVariantField(qpB, field, namePrefix);
+ }
+ }
+
+ private void addField(QueryProfilesConfig.Queryprofile.Builder qpB,
+ QueryProfile profile, Entry<String, Object> field, String namePrefix) {
+ String fullName=namePrefix + field.getKey();
+ if (field.getValue() instanceof QueryProfile) {
+ QueryProfile subProfile=(QueryProfile)field.getValue();
+ if ( ! subProfile.isExplicit()) { // Implicitly defined profile - add content
+ addFieldChildren(qpB, subProfile,fullName + ".");
+ }
+ else { // Reference to an id'ed profile - output reference plus any local overrides
+ QueryProfilesConfig.Queryprofile.Reference.Builder refB = new QueryProfilesConfig.Queryprofile.Reference.Builder();
+ createReferenceFieldConfig(refB, profile, fullName, field.getKey(), ((BackedOverridableQueryProfile) subProfile).getBacking().getId().stringValue());
+ qpB.reference(refB);
+ addFieldChildren(qpB, subProfile,fullName + ".");
+ }
+ }
+ else { // a primitive
+ qpB.property(createPropertyFieldConfig(profile, fullName, field.getKey(), field.getValue()));
+ }
+ }
+
+ private void addVariantField(QueryProfilesConfig.Queryprofile.Queryprofilevariant.Builder qpB,
+ Entry<String, Object> field, String namePrefix) {
+ String fullName=namePrefix + field.getKey();
+ if (field.getValue() instanceof QueryProfile) {
+ QueryProfile subProfile=(QueryProfile)field.getValue();
+ if ( ! subProfile.isExplicit()) { // Implicitly defined profile - add content
+ addVariantFieldChildren(qpB, subProfile,fullName + ".");
+ }
+ else { // Reference to an id'ed profile - output reference plus any local overrides
+ QueryProfilesConfig.Queryprofile.Queryprofilevariant.Reference.Builder refB = new QueryProfilesConfig.Queryprofile.Queryprofilevariant.Reference.Builder();
+ createVariantReferenceFieldConfig(refB, fullName, ((BackedOverridableQueryProfile) subProfile).getBacking().getId().stringValue());
+ qpB.reference(refB);
+ addVariantFieldChildren(qpB, subProfile,fullName + ".");
+ }
+ }
+ else { // a primitive
+ qpB.property(createVariantPropertyFieldConfig(fullName, field.getValue()));
+ }
+ }
+
+ private void addVariants(QueryProfilesConfig.Queryprofile.Builder qB, QueryProfile profile) {
+ if (profile.getVariants()==null) return;
+ DeclaredQueryProfileVariants declaredVariants=new DeclaredQueryProfileVariants(profile);
+ for (DeclaredQueryProfileVariants.VariantQueryProfile variant : declaredVariants.getVariantQueryProfiles().values()) {
+ QueryProfilesConfig.Queryprofile.Queryprofilevariant.Builder varB = new QueryProfilesConfig.Queryprofile.Queryprofilevariant.Builder();
+ for (String dimensionValue : variant.getDimensionValues()) {
+ if (dimensionValue==null)
+ dimensionValue="*";
+ varB.fordimensionvalues(dimensionValue);
+ }
+ for (QueryProfile inherited : variant.inherited())
+ varB.inherit(inherited.getId().stringValue());
+
+ List<Map.Entry<String,Object>> content=new ArrayList<>(variant.getValues().entrySet());
+ Collections.sort(content,new MapEntryKeyComparator());
+ for (Map.Entry<String,Object> value : content) {
+ addVariantField(varB, value,"");
+ }
+ qB.queryprofilevariant(varB);
+ }
+ }
+
+ private void createReferenceFieldConfig(QueryProfilesConfig.Queryprofile.Reference.Builder refB, QueryProfile profile,
+ String fullName, String localName, String stringValue) {
+ refB.name(fullName);
+ if (stringValue!=null) refB.value(stringValue);
+ Boolean overridable=null;
+ if (profile!=null)
+ overridable=profile.isDeclaredOverridable(localName, null);
+ if (overridable!=null)
+ refB.overridable(""+overridable);
+ }
+
+ private void createVariantReferenceFieldConfig(QueryProfilesConfig.Queryprofile.Queryprofilevariant.Reference.Builder refB,
+ String fullName, String stringValue) {
+ refB.name(fullName);
+ if (stringValue!=null) refB.value(stringValue);
+ }
+
+ private QueryProfilesConfig.Queryprofile.Property.Builder createPropertyFieldConfig(
+ QueryProfile profile, String fullName, String localName, Object value) {
+ QueryProfilesConfig.Queryprofile.Property.Builder propB = new QueryProfilesConfig.Queryprofile.Property.Builder();
+ Boolean overridable=null;
+ if (value instanceof SubstituteString)
+ value=value.toString(); // Send only types understood by configBuilder downwards
+ propB.name(fullName);
+ if (value!=null) propB.value(value.toString());
+ if (profile!=null)
+ overridable=profile.isDeclaredOverridable(localName, null);
+ if (overridable!=null)
+ propB.overridable(""+overridable);
+ return propB;
+ }
+
+ private QueryProfilesConfig.Queryprofile.Queryprofilevariant.Property.Builder createVariantPropertyFieldConfig(
+ String fullName, Object value) {
+ QueryProfilesConfig.Queryprofile.Queryprofilevariant.Property.Builder propB = new QueryProfilesConfig.Queryprofile.Queryprofilevariant.Property.Builder();
+ if (value instanceof SubstituteString)
+ value=value.toString(); // Send only types understood by configBuilder downwards
+ propB.name(fullName);
+ if (value!=null) propB.value(value.toString());
+ return propB;
+ }
+
+ private QueryProfilesConfig.Queryprofiletype.Builder createConfig(QueryProfileType profileType) {
+ QueryProfilesConfig.Queryprofiletype.Builder qtB = new QueryProfilesConfig.Queryprofiletype.Builder();
+ qtB.id(profileType.getId().stringValue());
+ if (profileType.isDeclaredStrict())
+ qtB.strict(true);
+ if (profileType.getDeclaredMatchAsPath())
+ qtB.matchaspath(true);
+ for (QueryProfileType inherited : profileType.inherited())
+ qtB.inherit(inherited.getId().stringValue());
+ List<FieldDescription> fields=new ArrayList<>(profileType.declaredFields().values());
+ Collections.sort(fields);
+ for (FieldDescription field : fields)
+ qtB.field(createConfig(field));
+ return qtB;
+ }
+
+ private QueryProfilesConfig.Queryprofiletype.Field.Builder createConfig(FieldDescription field) {
+ QueryProfilesConfig.Queryprofiletype.Field.Builder fB = new QueryProfilesConfig.Queryprofiletype.Field.Builder();
+ fB.
+ name(field.getName()).
+ type(field.getType().stringValue());
+ if ( ! field.isOverridable())
+ fB.overridable(false);
+ if (field.isMandatory())
+ fB.mandatory(true);
+ String aliases=toSpaceSeparatedString(field.getAliases());
+ if (!aliases.isEmpty())
+ fB.alias(aliases);
+ return fB;
+ }
+
+ public String toSpaceSeparatedString(List<String> list) {
+ StringBuilder b=new StringBuilder();
+ for (Iterator<String> i=list.iterator(); i.hasNext(); ) {
+ b.append(i.next());
+ if (i.hasNext())
+ b.append(" ");
+ }
+ return b.toString();
+ }
+
+ private static class MapEntryKeyComparator implements Comparator<Map.Entry<String,Object>> {
+
+ public int compare(Map.Entry<String,Object> e1,Map.Entry<String,Object> e2) {
+ return e1.getKey().compareTo(e2.getKey());
+ }
+
+ public boolean equals(Object other) {
+ return other instanceof MapEntryKeyComparator;
+ }
+
+ }
+
+ /**
+ * The config produced by this
+ * @return query profiles config
+ */
+ public QueryProfilesConfig getConfig() {
+ QueryProfilesConfig.Builder qB = new QueryProfilesConfig.Builder();
+ getConfig(qB);
+ return new QueryProfilesConfig(qB);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfilesBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfilesBuilder.java
new file mode 100644
index 00000000000..26e6ae75999
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/QueryProfilesBuilder.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.search.query.profile.config.QueryProfileXMLReader;
+import com.yahoo.config.application.api.ApplicationPackage;
+
+import java.util.List;
+
+/**
+ * Reads the query profile and query profile types from an application package. The actual reading
+ * is delegated to a {@link com.yahoo.search.query.profile.config.QueryProfileXMLReader}.
+ *
+ * @author bratseth
+ */
+// TODO: Move into QueryProfiles
+public class QueryProfilesBuilder {
+
+ /** Build the set of query profiles for an application package */
+ public QueryProfiles build(ApplicationPackage applicationPackage) {
+ List<NamedReader> queryProfileTypeFiles=null;
+ List<NamedReader> queryProfileFiles=null;
+ try {
+ queryProfileTypeFiles=applicationPackage.getQueryProfileTypeFiles();
+ queryProfileFiles=applicationPackage.getQueryProfileFiles();
+ return new QueryProfiles(new QueryProfileXMLReader().read(queryProfileTypeFiles,queryProfileFiles));
+ }
+ finally {
+ NamedReader.closeAll(queryProfileTypeFiles);
+ NamedReader.closeAll(queryProfileFiles);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRuleBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRuleBuilder.java
new file mode 100644
index 00000000000..3969054c1d1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRuleBuilder.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.io.IOUtils;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.prelude.semantics.RuleBase;
+import com.yahoo.prelude.semantics.RuleImporter;
+import com.yahoo.prelude.semantics.parser.ParseException;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Reads the semantic rules from the application package by delegating to RuleConfigDeriver.
+ *
+ * @author bratseth
+ */
+// TODO: Move into SemanticRules
+public class SemanticRuleBuilder {
+
+ /** Build the set of semantic rules for an application package */
+ public SemanticRules build(ApplicationPackage applicationPackage) {
+ List<NamedReader> ruleBaseFiles = null;
+ try {
+ ruleBaseFiles = applicationPackage.getFiles(ApplicationPackage.RULES_DIR, "sr");
+ return new SemanticRules(ruleBaseFiles.stream().map(this::toRuleBaseConfigView).collect(Collectors.toList()));
+ }
+ finally {
+ NamedReader.closeAll(ruleBaseFiles);
+ }
+ }
+
+ private SemanticRules.RuleBase toRuleBaseConfigView(NamedReader reader) {
+ try {
+ String ruleBaseString = IOUtils.readAll(reader.getReader());
+ boolean isDefault = ruleBaseString.contains("@default");
+ return new SemanticRules.RuleBase(toName(reader.getName()), isDefault, ruleBaseString);
+ }
+ catch (IOException e) {
+ throw new IllegalArgumentException("Could not load rules bases", e);
+ }
+ }
+
+ private String toName(String fileName) {
+ String shortName = new File(fileName).getName();
+ return shortName.substring(0, shortName.length()-".sr".length());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRules.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRules.java
new file mode 100644
index 00000000000..4e24ffd8b19
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/SemanticRules.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.prelude.semantics.SemanticRulesConfig;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Returns the semantic rules config form a set of rule bases.
+ * Owned by a container cluster
+ *
+ * @author bratseth
+ */
+public class SemanticRules implements Serializable, SemanticRulesConfig.Producer {
+
+ private List<RuleBase> ruleBases;
+
+ public SemanticRules(List<RuleBase> ruleBases) {
+ this.ruleBases = ruleBases;
+ }
+
+ @Override
+ public void getConfig(SemanticRulesConfig.Builder builder) {
+ for (RuleBase ruleBase : ruleBases)
+ builder.rulebase(ruleBase.getConfig());
+ }
+
+ /** A config view of a rule base */
+ public static class RuleBase {
+
+ private final String name;
+ private final boolean isDefault;
+ private final String rules;
+
+ public RuleBase(String name, boolean isDefault, String rules) {
+ this.name = name;
+ this.isDefault = isDefault;
+ this.rules = rules;
+ }
+
+ private SemanticRulesConfig.Rulebase.Builder getConfig() {
+ SemanticRulesConfig.Rulebase.Builder ruleBaseBuilder = new SemanticRulesConfig.Rulebase.Builder();
+ ruleBaseBuilder.name(name);
+ ruleBaseBuilder.isdefault(isDefault);
+ ruleBaseBuilder.rules(rules);
+ return ruleBaseBuilder;
+ }
+
+
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/FederationSearcher.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/FederationSearcher.java
new file mode 100644
index 00000000000..0dbc7368954
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/FederationSearcher.java
@@ -0,0 +1,269 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.collections.CollectionUtil;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.search.searchchain.model.federation.FederationSearcherModel;
+import com.yahoo.search.federation.FederationConfig;
+import com.yahoo.search.searchchain.model.federation.FederationSearcherModel.TargetSpec;
+import com.yahoo.vespa.model.container.component.Component;
+
+import java.util.*;
+
+/**
+ * Config producer for the FederationSearcher.
+ * @author tonytv
+ */
+public class FederationSearcher extends Searcher<FederationSearcherModel> implements FederationConfig.Producer {
+
+ private final Optional<Component> targetSelector;
+
+ /**
+ * Generates config for a single search chain contained in a target.
+ */
+ private static final class SearchChainConfig {
+ private final SearchChain searchChain;
+ //Zero if not applicable
+ final ComponentId providerId;
+ final FederationOptions targetOptions;
+ final List<String> documentTypes;
+
+ SearchChainConfig(SearchChain searchChain, ComponentId providerId,
+ FederationOptions targetOptions, List<String> documentTypes) {
+ this.searchChain = searchChain;
+ this.providerId = providerId;
+ this.targetOptions = targetOptions;
+ this.documentTypes = documentTypes;
+ }
+
+ public FederationConfig.Target.SearchChain.Builder getSearchChainConfig() {
+ FederationConfig.Target.SearchChain.Builder sB = new FederationConfig.Target.SearchChain.Builder();
+ FederationOptions resolvedOptions = targetOptions.inherit(searchChain.federationOptions());
+ sB.
+ searchChainId(searchChain.getGlobalComponentId().stringValue()).
+ timeoutMillis(resolvedOptions.getTimeoutInMilliseconds()).
+ requestTimeoutMillis(resolvedOptions.getRequestTimeoutInMilliseconds()).
+ optional(resolvedOptions.getOptional()).
+ useByDefault(resolvedOptions.getUseByDefault()).
+ documentTypes(documentTypes);
+ if (providerId != null)
+ sB.providerId(providerId.stringValue());
+ return sB;
+ }
+ }
+
+ /**
+ * One or more search chains that are handled as a single group,
+ * which can be federated to as a single entity.
+ */
+ private static abstract class Target {
+ final ComponentId id;
+ final FederationOptions targetOptions;
+
+ public Target(ComponentId id, FederationOptions targetOptions) {
+ this.id = id;
+ this.targetOptions = targetOptions;
+ }
+
+ public FederationConfig.Target.Builder getTargetConfig() {
+ FederationConfig.Target.Builder tb = new FederationConfig.Target.Builder();
+ tb.
+ id(id.stringValue()).
+ useByDefault(targetOptions.getUseByDefault());
+ getSearchChainsConfig(tb);
+ return tb;
+ }
+
+ protected abstract void getSearchChainsConfig(FederationConfig.Target.Builder tb);
+ }
+
+ private static class SearchChainTarget extends Target {
+ private final SearchChainConfig searchChainConfig;
+
+ public SearchChainTarget(SearchChain searchChain,
+ FederationOptions targetOptions) {
+ super(searchChain.getComponentId(), targetOptions);
+ searchChainConfig = new SearchChainConfig(
+ searchChain,
+ null,
+ targetOptions,
+ searchChain.getDocumentTypes());
+ }
+
+ @Override
+ protected void getSearchChainsConfig(FederationConfig.Target.Builder tB) {
+ tB.searchChain(searchChainConfig.getSearchChainConfig());
+ }
+ }
+
+ private static class SourceGroupTarget extends Target {
+ private final SearchChainConfig leaderConfig;
+ private final List<SearchChainConfig> participantsConfig =
+ new ArrayList<>();
+
+ public SourceGroupTarget(SourceGroup group,
+ FederationOptions targetOptions) {
+ super(group.getComponentId(), applyDefaultSourceGroupOptions(targetOptions));
+
+ leaderConfig = createConfig(group.leader(), targetOptions);
+ for (Source participant : group.participants()) {
+ participantsConfig.add(
+ createConfig(participant, targetOptions));
+ }
+ }
+
+ private static FederationOptions applyDefaultSourceGroupOptions(FederationOptions targetOptions) {
+ FederationOptions defaultSourceGroupOption = new FederationOptions().setUseByDefault(true);
+ return targetOptions.inherit(defaultSourceGroupOption);
+ }
+
+ private SearchChainConfig createConfig(Source source,
+ FederationOptions targetOptions) {
+ return new SearchChainConfig(
+ source,
+ source.getParentProvider().getComponentId(),
+ targetOptions,
+ source.getDocumentTypes());
+ }
+
+ @Override
+ protected void getSearchChainsConfig(FederationConfig.Target.Builder tB) {
+ tB.searchChain(leaderConfig.getSearchChainConfig());
+ for (SearchChainConfig participant : participantsConfig) {
+ tB.searchChain(participant.getSearchChainConfig());
+ }
+ }
+ }
+
+ private static class TargetResolver {
+ final ComponentRegistry<SearchChain> searchChainRegistry;
+ final SourceGroupRegistry sourceGroupRegistry;
+
+ /**
+ * @return true if searchChain.id newer than sourceGroup.id
+ */
+ private boolean newerVersion(SearchChain searchChain,
+ SourceGroup sourceGroup) {
+ if (searchChain == null || sourceGroup == null) {
+ return false;
+ } else {
+ return newerVersion(searchChain.getComponentId(), sourceGroup.getComponentId());
+ }
+ }
+
+ /**
+ * @return true if a newer than b
+ */
+ private boolean newerVersion(ComponentId a, ComponentId b) {
+ return a.compareTo(b) > 0;
+ }
+
+
+ TargetResolver(ComponentRegistry<SearchChain> searchChainRegistry,
+ SourceGroupRegistry sourceGroupRegistry) {
+ this.searchChainRegistry = searchChainRegistry;
+ this.sourceGroupRegistry = sourceGroupRegistry;
+ }
+
+ Target resolve(FederationSearcherModel.TargetSpec specification) {
+ SearchChain searchChain = searchChainRegistry.getComponent(
+ specification.sourceSpec);
+ SourceGroup sourceGroup = sourceGroupRegistry.getComponent(
+ specification.sourceSpec);
+
+ if (searchChain == null && sourceGroup == null) {
+ return null;
+ } else if (sourceGroup == null ||
+ newerVersion(searchChain, sourceGroup)) {
+ return new SearchChainTarget(searchChain, specification.federationOptions);
+ } else {
+ return new SourceGroupTarget(sourceGroup, specification.federationOptions);
+ }
+ }
+ }
+
+ private final Map<ComponentId, Target> resolvedTargets =
+ new LinkedHashMap<>();
+
+ public FederationSearcher(FederationSearcherModel searcherModel, Optional<Component> targetSelector) {
+ super(searcherModel);
+ this.targetSelector = targetSelector;
+
+ if (targetSelector.isPresent())
+ addChild(targetSelector.get());
+ }
+
+ @Override
+ public void getConfig(FederationConfig.Builder builder) {
+ for (Target target : resolvedTargets.values()) {
+ builder.target(target.getTargetConfig());
+ }
+
+ if (targetSelector.isPresent()) {
+ builder.targetSelector(targetSelector.get().getGlobalComponentId().stringValue());
+ }
+ }
+
+ @Override
+ public void initialize() {
+ initialize(getSearchChains().allChains(), getSearchChains().allSourceGroups());
+ }
+
+ void initialize(ComponentRegistry<SearchChain> searchChainRegistry,
+ SourceGroupRegistry sourceGroupRegistry) {
+ TargetResolver targetResolver = new TargetResolver(
+ searchChainRegistry, sourceGroupRegistry);
+
+ addSourceTargets(targetResolver, model.targets);
+
+ if (model.inheritDefaultSources)
+ addDefaultTargets(targetResolver, searchChainRegistry);
+ }
+
+ private void addSourceTargets(TargetResolver targetResolver, List<TargetSpec> targets) {
+ for (TargetSpec targetSpec : targets) {
+
+ Target target = targetResolver.resolve(targetSpec);
+ if (target == null) {
+ throw new RuntimeException("Can't find source " +
+ targetSpec.sourceSpec +
+ " used as a source for federation '" +
+ getComponentId() + "'");
+ }
+
+ Target duplicate = resolvedTargets.put(target.id, target);
+ if (duplicate != null && !duplicate.targetOptions.equals(target.targetOptions)) {
+ throw new RuntimeException("Search chain " + target.id + " added twice with different federation options"
+ + " to the federation searcher " + getComponentId());
+ }
+ }
+ }
+
+
+ private void addDefaultTargets(TargetResolver targetResolver, ComponentRegistry<SearchChain> searchChainRegistry) {
+ for (GenericTarget genericTarget : defaultTargets(searchChainRegistry.allComponents())) {
+ ComponentSpecification specification = genericTarget.getComponentId().toSpecification();
+
+ //Can't use genericTarget directly, as it might be part of a source group.
+ Target federationTarget = targetResolver.resolve(new TargetSpec(specification, new FederationOptions()));
+ //Do not replace manually added sources, as they might have manually configured federation options
+ if (!resolvedTargets.containsKey(federationTarget.id))
+ resolvedTargets.put(federationTarget.id, federationTarget);
+ }
+ }
+
+
+ private static List<GenericTarget> defaultTargets(Collection<SearchChain> allSearchChains) {
+ Collection<Provider> providers =
+ CollectionUtil.filter(allSearchChains, Provider.class);
+
+ List<GenericTarget> defaultTargets = new ArrayList<>();
+ for (Provider provider : providers) {
+ defaultTargets.addAll(provider.defaultFederationTargets());
+ }
+ return defaultTargets;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/GenericTarget.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/GenericTarget.java
new file mode 100644
index 00000000000..9ed62f15244
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/GenericTarget.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+
+/**
+ * A search chain that is intended to be used for federation (i.e. providers, sources)
+ * @author tonytv
+ */
+abstract public class GenericTarget extends SearchChain {
+
+ private final FederationOptions federationOptions;
+
+ public GenericTarget(ChainSpecification specWithoutInnerSearchers, FederationOptions federationOptions) {
+ super(specWithoutInnerSearchers);
+ this.federationOptions = federationOptions;
+ }
+
+ @Override
+ public FederationOptions federationOptions() {
+ FederationOptions defaultOptions = new FederationOptions().setUseByDefault(useByDefault());
+ return federationOptions.inherit(defaultOptions);
+ }
+
+ /** The value for useByDefault in case the user have not specified any **/
+ abstract protected boolean useByDefault();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProvider.java
new file mode 100644
index 00000000000..ef6c6ef4df6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProvider.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.binaryprefix.BinaryPrefix;
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.search.cache.QrBinaryCacheConfig;
+import com.yahoo.search.cache.QrBinaryCacheRegionConfig;
+import com.yahoo.search.federation.ProviderConfig;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.search.searchchain.model.federation.HttpProviderSpec;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.yahoo.search.federation.ProviderConfig.Node;
+import static com.yahoo.search.federation.ProviderConfig.Yca;
+
+
+/**
+ * A provider containing a http searcher.
+ * @author tonytv
+ */
+public class HttpProvider extends Provider implements ProviderConfig.Producer,
+ QrBinaryCacheConfig.Producer,
+ QrBinaryCacheRegionConfig.Producer {
+
+ private final HttpProviderSpec providerSpec;
+
+ //TODO: For backward compatibility only, eliminate this later
+ private BinaryScaledAmount cacheSize;
+
+ public double getCacheWeight() {
+ return providerSpec.cacheWeight;
+ }
+
+ /**
+ * TODO: remove, for backward compatibility only.
+ */
+ public void setCacheSize(BinaryScaledAmount cacheSize) {
+ this.cacheSize = cacheSize;
+ }
+
+ /*
+ * Config producer for the contained http searcher..
+ */
+
+ public HttpProvider(ChainSpecification specWithoutInnerSearchers, FederationOptions federationOptions, HttpProviderSpec providerSpec) {
+ super(specWithoutInnerSearchers, federationOptions);
+ this.providerSpec = providerSpec;
+ }
+
+ @Override
+ public void getConfig(ProviderConfig.Builder builder) {
+ if (providerSpec.path != null)
+ builder.path(providerSpec.path);
+ if (providerSpec.connectionParameters.readTimeout != null)
+ builder.readTimeout(providerSpec.connectionParameters.readTimeout );
+ if (providerSpec.connectionParameters.connectionTimeout != null)
+ builder.connectionTimeout(providerSpec.connectionParameters.connectionTimeout);
+ if (providerSpec.connectionParameters.connectionPoolTimeout != null)
+ builder.connectionPoolTimeout(providerSpec.connectionParameters.connectionPoolTimeout);
+ if (providerSpec.connectionParameters.retries != null)
+ builder.retries(providerSpec.connectionParameters.retries);
+
+ builder.node(getNodes(providerSpec.nodes));
+
+ if (providerSpec.ycaApplicationId != null) {
+ builder.yca(getYca(providerSpec));
+ }
+ }
+
+ private static Yca.Builder getYca(HttpProviderSpec providerSpec) {
+ Yca.Builder yca = new Yca.Builder()
+ .applicationId(providerSpec.ycaApplicationId);
+
+ if (providerSpec.ycaProxy != null) {
+ yca.useProxy(true);
+ if (providerSpec.ycaProxy.host != null) {
+ yca.host(providerSpec.ycaProxy.host)
+ .port(providerSpec.ycaProxy.port);
+ }
+ }
+ if (providerSpec.ycaCertificateTtl != null) yca.ttl(providerSpec.ycaCertificateTtl);
+ if (providerSpec.ycaRetryWait != null) yca.ttl(providerSpec.ycaRetryWait);
+ return yca;
+ }
+
+ private static List<Node.Builder> getNodes(List<HttpProviderSpec.Node> nodeSpecs) {
+ ArrayList<Node.Builder> nodes = new ArrayList<>();
+ for (HttpProviderSpec.Node node : nodeSpecs) {
+ nodes.add(
+ new Node.Builder()
+ .host(node.host)
+ .port(node.port));
+ }
+ return nodes;
+ }
+
+ public int cacheSizeMB() {
+ return providerSpec.cacheSizeMB != null ?
+ providerSpec.cacheSizeMB :
+ (int) cacheSize.as(BinaryPrefix.mega);
+ }
+
+ @Override
+ public void getConfig(QrBinaryCacheConfig.Builder builder) {
+ builder.cache_size(cacheSizeMB());
+ }
+
+ @Override
+ public void getConfig(QrBinaryCacheRegionConfig.Builder builder) {
+ builder.region_size(cacheSizeMB());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProviderSearcher.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProviderSearcher.java
new file mode 100644
index 00000000000..44f9879230d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/HttpProviderSearcher.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.binaryprefix.BinaryPrefix;
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.search.searchchain.model.federation.HttpProviderSpec;
+import java.util.List;
+
+/**
+
+* @author tonytv
+*/
+public class HttpProviderSearcher extends Searcher<ChainedComponentModel> {
+
+
+ public HttpProviderSearcher(ChainedComponentModel model) {
+ super(model);
+ }
+
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/LocalProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/LocalProvider.java
new file mode 100644
index 00000000000..1dd4fb478ec
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/LocalProvider.java
@@ -0,0 +1,188 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
+import com.yahoo.prelude.cluster.QrMonitorConfig;
+import com.yahoo.search.config.dispatchprototype.SearchNodesConfig;
+import com.yahoo.vespa.config.search.DispatchConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.search.config.ClusterConfig;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.search.searchchain.model.federation.LocalProviderSpec;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import com.yahoo.vespa.model.search.SearchNode;
+
+import java.util.*;
+
+/**
+ * Config producer for search chain responsible for sending queries to a local cluster.
+ *
+ * @author tonytv
+ */
+public class LocalProvider extends Provider implements
+ DocumentdbInfoConfig.Producer,
+ ClusterConfig.Producer,
+ AttributesConfig.Producer,
+ QrMonitorConfig.Producer,
+ RankProfilesConfig.Producer,
+ SearchNodesConfig.Producer,
+ DispatchConfig.Producer {
+
+ private final LocalProviderSpec providerSpec;
+ private volatile AbstractSearchCluster searchCluster;
+
+
+ @Override
+ public void getConfig(ClusterConfig.Builder builder) {
+ assert (searchCluster != null) : "Null search cluster!";
+ builder.clusterId(searchCluster.getClusterIndex());
+ builder.clusterName(searchCluster.getClusterName());
+
+ if (providerSpec.cacheSize != null)
+ builder.cacheSize(providerSpec.cacheSize);
+
+ if (searchCluster.getVisibilityDelay() != null)
+ builder.cacheTimeout(convertVisibilityDelay(searchCluster.getVisibilityDelay()));
+ }
+
+ @Override
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ searchCluster.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ searchCluster.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(QrMonitorConfig.Builder builder) {
+ int requestTimeout = federationOptions().getTimeoutInMilliseconds();
+ if (requestTimeout != -1) {
+ builder.requesttimeout(requestTimeout);
+ }
+ }
+
+ @Override
+ public void getConfig(final SearchNodesConfig.Builder builder) {
+ if (!(searchCluster instanceof IndexedSearchCluster)) {
+ log.warning("Could not build SearchNodesConfig: Only supported for IndexedSearchCluster, got "
+ + searchCluster.getClass().getCanonicalName());
+ return;
+ }
+ final IndexedSearchCluster indexedSearchCluster = (IndexedSearchCluster) searchCluster;
+ for (final SearchNode searchNode : indexedSearchCluster.getSearchNodes()) {
+ builder.search_node(
+ new SearchNodesConfig.Search_node.Builder()
+ .host(searchNode.getHostName())
+ .port(searchNode.getDispatchPort()));
+ }
+ }
+
+ private void addProviderSearchers(LocalProviderSpec providerSpec) {
+ for (ChainedComponentModel searcherModel : providerSpec.searcherModels) {
+ addInnerComponent(new Searcher<>(searcherModel));
+ }
+ }
+
+ @Override
+ public ChainSpecification getChainSpecification() {
+ ChainSpecification spec =
+ super.getChainSpecification();
+ return new ChainSpecification(spec.componentId, spec.inheritance, spec.phases(),
+ disableStemmingIfStreaming(spec.componentReferences));
+ }
+
+ //TODO: ugly, restructure this
+ private Set<ComponentSpecification> disableStemmingIfStreaming(Set<ComponentSpecification> searcherReferences) {
+ if (!searchCluster.isStreaming()) {
+ return searcherReferences;
+ } else {
+ Set<ComponentSpecification> filteredSearcherReferences = new LinkedHashSet<>(searcherReferences);
+ filteredSearcherReferences.remove(
+ toGlobalComponentId(
+ new ComponentId("com.yahoo.prelude.querytransform.StemmingSearcher")).
+ toSpecification());
+ return filteredSearcherReferences;
+ }
+ }
+
+ private ComponentId toGlobalComponentId(ComponentId searcherId) {
+ return searcherId.nestInNamespace(getComponentId());
+ }
+
+ public String getClusterName() {
+ return providerSpec.clusterName;
+ }
+
+ public void setSearchCluster(AbstractSearchCluster searchCluster) {
+ assert (this.searchCluster == null);
+ this.searchCluster = searchCluster;
+ }
+
+ public LocalProvider(ChainSpecification specWithoutInnerSearchers,
+ FederationOptions federationOptions,
+ LocalProviderSpec providerSpec) {
+ super(specWithoutInnerSearchers, federationOptions);
+ addProviderSearchers(providerSpec);
+ this.providerSpec = providerSpec;
+ }
+
+ @Override
+ public List<String> getDocumentTypes() {
+ List<String> documentTypes = new ArrayList<>();
+
+ for (AbstractSearchCluster.SearchDefinitionSpec spec : searchCluster.getLocalSDS()) {
+ documentTypes.add(spec.getSearchDefinition().getSearch().getDocument().getName());
+ }
+
+ return documentTypes;
+ }
+
+ @Override
+ public FederationOptions federationOptions() {
+ Double queryTimeoutInSeconds = searchCluster.getQueryTimeout();
+
+ return queryTimeoutInSeconds == null ?
+ super.federationOptions() :
+ super.federationOptions().inherit(
+ new FederationOptions().setTimeoutInMilliseconds((int) (queryTimeoutInSeconds * 1000)));
+ }
+
+ @Override
+ public void getConfig(DocumentdbInfoConfig.Builder builder) {
+ searchCluster.getConfig(builder);
+ }
+
+ /**
+ * For backward compatibility only, do not use.
+ */
+ public void setCacheSize(Integer cacheSize) {
+ providerSpec.cacheSize = cacheSize;
+ }
+
+ // The semantics of visibility delay in search is deactivating caches if the
+ // delay is less than 1.0, in qrs the cache is deactivated if the delay is 0
+ // (or less). 1.0 seems a little arbitrary, so just doing the conversion
+ // here instead of having two totally independent implementations having to
+ // follow each other down in the modules.
+ private static Double convertVisibilityDelay(Double visibilityDelay) {
+ return (visibilityDelay < 1.0d) ? 0.0d : visibilityDelay;
+ }
+
+ @Override
+ public void getConfig(DispatchConfig.Builder builder) {
+ if (!(searchCluster instanceof IndexedSearchCluster)) {
+ log.warning("Could not build DispatchConfig: Only supported for IndexedSearchCluster, got "
+ + searchCluster.getClass().getCanonicalName());
+ return;
+ }
+ ((IndexedSearchCluster) searchCluster).getConfig(builder);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Provider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Provider.java
new file mode 100644
index 00000000000..8dba2e8b589
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Provider.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.vespa.model.container.component.ConfigProducerGroup;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Base config producer for search chains that communicate with backends.
+ *
+ * @author tonytv
+ */
+public class Provider extends GenericTarget {
+
+ private ConfigProducerGroup<Source> sources;
+
+ public Provider(ChainSpecification specWithoutInnerSearchers, FederationOptions federationOptions) {
+ super(specWithoutInnerSearchers, federationOptions);
+ sources = new ConfigProducerGroup<>(this, "source");
+ }
+
+ public void addSource(Source source) {
+ sources.addComponent(source.getComponentId(), source);
+ }
+
+ public Collection<Source> getSources() {
+ return sources.getComponents();
+ }
+
+ @Override
+ protected boolean useByDefault() {
+ return sources.getComponents().isEmpty();
+ }
+
+ public Collection<? extends GenericTarget> defaultFederationTargets() {
+ if (sources.getComponents().isEmpty()) {
+ return Arrays.asList(this);
+ } else {
+ return sources.getComponents();
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChain.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChain.java
new file mode 100644
index 00000000000..148bab3f84a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChain.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a search chain in the vespa model.
+ *
+ * @author tonytv
+ */
+public class SearchChain extends Chain<Searcher<?>> {
+
+ public SearchChain(ChainSpecification specWithoutInnerSearchers) {
+ super(specWithoutInnerSearchers);
+ }
+
+ public FederationOptions federationOptions() {
+ return new FederationOptions().setUseByDefault(true);
+ }
+
+ //A list of documents types that this search chain provides results for, empty if unknown
+ public List<String> getDocumentTypes() {
+ return Collections.emptyList();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChains.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChains.java
new file mode 100644
index 00000000000..d13e13b232f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SearchChains.java
@@ -0,0 +1,129 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+import com.yahoo.collections.CollectionUtil;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.chain.Chains;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.container.search.searchchain.defaultsearchchains.LocalClustersCreator;
+import com.yahoo.vespa.model.container.search.searchchain.defaultsearchchains.VespaSearchChainsCreator;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Root config producer of the whole search chains model (contains searchchains and searchers).
+ *
+ * @author tonytv
+ */
+public class SearchChains extends Chains<SearchChain> {
+
+ private final SourceGroupRegistry sourceGroups = new SourceGroupRegistry();
+
+ public SearchChains(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ }
+
+ public void initialize(Map<String, ? extends AbstractSearchCluster> searchClustersByName, BinaryScaledAmount totalProviderCacheSize) {
+ LocalClustersCreator.addDefaultLocalProviders(this, searchClustersByName.keySet());
+ VespaSearchChainsCreator.addVespaSearchChains(this);
+
+ validateSourceGroups(); //must be done before initializing searchers since they are used by FederationSearchers.
+ initializeComponents(searchClustersByName, totalProviderCacheSize);
+ }
+
+ private void initializeComponents(Map<String, ? extends AbstractSearchCluster> searchClustersByName,
+ BinaryScaledAmount totalProviderCacheSize) {
+ setSearchClusterForLocalProvider(searchClustersByName);
+ setCacheSizeForHttpProviders(totalProviderCacheSize);
+ initializeComponents();
+ }
+
+ private void setCacheSizeForHttpProviders(BinaryScaledAmount totalProviderCacheSize) {
+ double totalCacheWeight = 0;
+ for (HttpProvider provider : httpProviders()) {
+ totalCacheWeight += provider.getCacheWeight();
+ }
+
+ final BinaryScaledAmount cacheUnit = totalProviderCacheSize.divide(totalCacheWeight);
+ for (HttpProvider provider : httpProviders()) {
+ provider.setCacheSize(cacheUnit.multiply(provider.getCacheWeight()));
+ }
+ }
+
+ private void setSearchClusterForLocalProvider(Map<String, ? extends AbstractSearchCluster> clusterIndexByName) {
+ for (LocalProvider provider : localProviders()) {
+ AbstractSearchCluster cluster = clusterIndexByName.get(provider.getClusterName());
+ if (cluster == null) {
+ throw new RuntimeException("No searchable content cluster with id '" + provider.getClusterName() + "'");
+ }
+ provider.setSearchCluster(cluster);
+ }
+ }
+
+ private void validateSourceGroups() {
+ for (SourceGroup sourceGroup : sourceGroups.groups()) {
+ sourceGroup.validate();
+
+ if (getChainGroup().getComponentMap().containsKey(sourceGroup.getComponentId())) {
+ throw new RuntimeException(
+ String.format("Same id used for a source and another search chain/provider: '%s'",
+ sourceGroup.getComponentId()));
+ }
+ }
+ }
+
+ @Override
+ public void validate() throws Exception {
+ validateSourceGroups();
+ super.validate();
+ }
+
+ public SourceGroupRegistry allSourceGroups() {
+ return sourceGroups;
+ }
+
+ public Collection<LocalProvider> localProviders() {
+ return CollectionUtil.filter(allChains().allComponents(), LocalProvider.class);
+ }
+
+
+ public Collection<HttpProvider> httpProviders() {
+ return CollectionUtil.filter(allChains().allComponents(), HttpProvider.class);
+ }
+
+ /*
+ * If searchChain is a provider, its sources must already have been attached.
+ */
+ @Override
+ public void add(SearchChain searchChain) {
+ assert !(searchChain instanceof Source);
+
+ super.add(searchChain);
+
+ if (searchChain instanceof Provider) {
+ sourceGroups.addSources((Provider)searchChain);
+ }
+ }
+
+ @Override
+ public ComponentRegistry<SearchChain> allChains() {
+ ComponentRegistry<SearchChain> allChains = new ComponentRegistry<>();
+ for (SearchChain chain : getChainGroup().getComponents()) {
+ allChains.register(chain.getId(), chain);
+ if (chain instanceof Provider)
+ addSources(allChains, (Provider)chain);
+ }
+ allChains.freeze();
+ return allChains;
+ }
+
+ private void addSources(ComponentRegistry<SearchChain> chains, Provider provider) {
+ for (Source source : provider.getSources()) {
+ chains.register(source.getId(), source);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Searcher.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Searcher.java
new file mode 100644
index 00000000000..653be591be3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Searcher.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ */
+public class Searcher<T extends ChainedComponentModel> extends ChainedComponent<T> {
+
+ public Searcher(T model) {
+ super(model);
+ }
+
+ protected SearchChains getSearchChains() {
+ AbstractConfigProducer ancestor = getParent();
+ while (!(ancestor instanceof SearchChains)) {
+ ancestor = ancestor.getParent();
+ }
+ return (SearchChains)ancestor;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Source.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Source.java
new file mode 100644
index 00000000000..fe839b904d2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/Source.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+import java.util.Arrays;
+
+
+/**
+ * Config producer for source, which is contained in a provider.
+ *
+ * @author tonytv
+ */
+public class Source extends GenericTarget {
+
+ //Each source group must have exactly one leader, and an arbitrary number of participants
+ public enum GroupOption {
+ leader,
+ participant
+ }
+
+ public final GroupOption groupOption;
+
+ public Source(ChainSpecification specWithoutInnerSearchers, FederationOptions federationOptions,
+ GroupOption groupOption) {
+ super(specWithoutInnerSearchers, federationOptions);
+ this.groupOption = groupOption;
+ }
+
+ @Override
+ public FederationOptions federationOptions() {
+ return super.federationOptions().inherit(getParentProvider().federationOptions());
+ }
+
+ @Override
+ protected boolean useByDefault() {
+ return false;
+ }
+
+ public Provider getParentProvider() {
+ AbstractConfigProducer parent = getParent();
+ while (!(parent instanceof Provider)) {
+ parent = parent.getParent();
+ }
+ return (Provider)parent;
+ }
+
+ @Override
+ public ChainSpecification getChainSpecification() {
+ return super.getChainSpecification().addInherits(
+ Arrays.asList(getParentProvider().getComponentId().toSpecification()));
+ }
+
+ public ComponentId getGlobalComponentId() {
+ return getComponentId().nestInNamespace(
+ getParentProvider().getComponentId());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroup.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroup.java
new file mode 100644
index 00000000000..799bba45b04
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroup.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.ComponentId;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * A set of sources with the same name,
+ * each associated with a different provider,
+ * that fills the same role.
+ * @author tonytv
+ */
+final class SourceGroup {
+ private final ComponentId id;
+ private Source leader;
+ private final Set<Source> participants =
+ new LinkedHashSet<>();
+
+ private void setLeader(Source leader) {
+ assert (validMember(leader));
+
+ if (this.leader != null) {
+ throw new IllegalStateException(
+ "There can not be two default providers for the source "
+ + id);
+ }
+
+ this.leader = leader;
+ }
+
+ private void addParticipant(Source source) {
+ assert (validMember(source));
+ assert (!source.equals(leader));
+
+ if (!participants.add(source)) {
+ throw new RuntimeException("Source added twice to the same group "
+ + source);
+ }
+ }
+
+ private boolean validMember(Source leader) {
+ return leader.getComponentId().equals(id);
+ }
+
+ public ComponentId getComponentId() {
+ return id;
+ }
+
+ public SourceGroup(ComponentId id) {
+ this.id = id;
+ }
+
+ public void add(Source source) {
+ assert source.getComponentId().equals(getComponentId()):
+ "Ids differ: " + source.getComponentId() + " -- " + getComponentId();
+
+ if (Source.GroupOption.leader == source.groupOption) {
+ setLeader(source);
+ } else {
+ addParticipant(source);
+ }
+ }
+
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("Source id: ").append(id).append("\n").
+ append("Leader provider: ").append(
+ leader.getParentProvider().getComponentId()).append("\n").
+ append("Participants:");
+
+ for (Source participant : participants) {
+ builder.append("\n").append(" Provider: ").append(
+ participant.getParentProvider().getComponentId());
+ }
+ return builder.toString();
+ }
+
+ public Source leader() {
+ return leader;
+ }
+
+ public Collection<Source> participants() {
+ return Collections.unmodifiableCollection(participants);
+ }
+
+ public void validate() {
+ if (leader == null)
+ throw new IllegalStateException("Missing leader for the source " + getComponentId() +
+ ". One of the sources must use the attribute id instead of idref.");
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupRegistry.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupRegistry.java
new file mode 100644
index 00000000000..fda962402d1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupRegistry.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.model.ComponentAdaptor;
+import com.yahoo.component.provider.ComponentRegistry;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+
+/**
+ * Owns all the source groups in the search chains model.
+ * @author tonytv
+ */
+class SourceGroupRegistry {
+ private final ComponentRegistry<ComponentAdaptor<SourceGroup>> sourceGroups
+ = new ComponentRegistry<>();
+
+ private void add(Source source) {
+ getGroup(source.getComponentId()).add(source);
+ }
+
+ private SourceGroup getGroup(ComponentId sourceId) {
+ ComponentAdaptor<SourceGroup> group =
+ sourceGroups.getComponent(sourceId);
+ if (group == null) {
+ group = new ComponentAdaptor<>(sourceId,
+ new SourceGroup(sourceId));
+ sourceGroups.register(group.getId(), group);
+ }
+ return group.model;
+ }
+
+ void addSources(Provider provider) {
+ for (Source source : provider.getSources()) {
+ add(source);
+ }
+ }
+
+ public Collection<SourceGroup> groups() {
+ List<SourceGroup> result = new ArrayList<>();
+ for (ComponentAdaptor<SourceGroup> group :
+ sourceGroups.allComponents()) {
+ result.add(group.model);
+ }
+ return result;
+ }
+
+ public SourceGroup getComponent(ComponentSpecification spec) {
+ ComponentAdaptor<SourceGroup> result = sourceGroups.getComponent(spec);
+ return (result != null)?
+ result.model :
+ null;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/LocalClustersCreator.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/LocalClustersCreator.java
new file mode 100644
index 00000000000..43f4a28ff30
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/LocalClustersCreator.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain.defaultsearchchains;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.search.searchchain.model.federation.LocalProviderSpec;
+import com.yahoo.vespa.model.container.search.searchchain.LocalProvider;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Adds default search chains for all local clusters not mentioned explicitly
+ * @author tonytv
+ */
+public class LocalClustersCreator {
+ static ChainSpecification emptySearchChainSpecification(String componentName) {
+ return new ChainSpecification(
+ new ComponentId(componentName),
+ VespaSearchChainsCreator.inheritsVespaPhases(), //TODO: refactor
+ Collections.<Phase>emptyList(),
+ Collections.<ComponentSpecification>emptySet());
+ }
+
+ static LocalProvider createDefaultLocalProvider(String clusterName) {
+ return new LocalProvider(emptySearchChainSpecification(clusterName), new FederationOptions(),
+ new LocalProviderSpec(clusterName, null));
+ }
+
+ static Set<String> presentClusters(SearchChains searchChains) {
+ Set<String> presentClusters = new LinkedHashSet<>();
+ for (LocalProvider provider : searchChains.localProviders()) {
+ presentClusters.add(provider.getClusterName());
+ }
+ return presentClusters;
+ }
+
+ public static void addDefaultLocalProviders(SearchChains searchChains, Set<String> clusterNames) {
+ Set<String> missingClusters = new LinkedHashSet<>(clusterNames);
+ missingClusters.removeAll(presentClusters(searchChains));
+
+ for (String clusterName : missingClusters) {
+ searchChains.add(createDefaultLocalProvider(clusterName));
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/VespaSearchChainsCreator.java b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/VespaSearchChainsCreator.java
new file mode 100644
index 00000000000..ed5fd3b759e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/search/searchchain/defaultsearchchains/VespaSearchChainsCreator.java
@@ -0,0 +1,141 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain.defaultsearchchains;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.search.searchchain.PhaseNames;
+import com.yahoo.search.searchchain.model.VespaSearchers;
+import com.yahoo.search.searchchain.model.federation.FederationSearcherModel;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.search.searchchain.*;
+
+import java.util.*;
+
+/**
+ * Creates the search chains vespaPhases, vespa and native.
+ *
+ * <p>TODO: refactor</p>
+ * @author tonytv
+ */
+public class VespaSearchChainsCreator {
+ private static class PhasesCreator {
+ private static Set<String> set(String successor) {
+ return successor == null ? null : new LinkedHashSet<>(Arrays.asList(successor));
+ }
+
+ private static String lastElement(String[] phases) {
+ return phases[phases.length - 1];
+ }
+
+ private static Phase createPhase(String phase, String before) {
+ return new Phase(phase, set(before), null);
+ }
+
+ public static Collection<Phase> linearPhases(String... phases) {
+ List<Phase> result = new ArrayList<>();
+
+ for (int i=0; i < phases.length - 1; ++i) {
+ result.add(
+ createPhase(phases[i], phases[i+1]));
+ }
+
+ if (phases.length > 0) {
+ result.add(
+ createPhase(lastElement(phases), null));
+ }
+
+ return result;
+ }
+ }
+
+ private static Set<ComponentSpecification> noSearcherReferences() {
+ return Collections.emptySet();
+ }
+
+ private static Collection<Phase> noPhases() {
+ return Collections.emptySet();
+ }
+
+ private static ChainSpecification.Inheritance inherits(ComponentId chainId) {
+ Set<ComponentSpecification> inheritsSet = new LinkedHashSet<>();
+ inheritsSet.add(chainId.toSpecification());
+ return new ChainSpecification.Inheritance(inheritsSet, null);
+ }
+
+ static ChainSpecification.Inheritance inheritsVespaPhases() {
+ return inherits(vespaPhasesSpecification().componentId);
+ }
+
+ private static void addInnerSearchers(SearchChain searchChain, Collection<ChainedComponentModel> searcherModels) {
+ for (ChainedComponentModel searcherModel : searcherModels) {
+ searchChain.addInnerComponent(createSearcher(searcherModel));
+ }
+ }
+
+ private static Searcher<? extends ChainedComponentModel> createSearcher(ChainedComponentModel searcherModel) {
+ if (searcherModel instanceof FederationSearcherModel) {
+ return new FederationSearcher((FederationSearcherModel) searcherModel, Optional.<Component>empty());
+ } else {
+ return new Searcher<>(searcherModel);
+ }
+ }
+
+
+ private static ChainSpecification nativeSearchChainSpecification() {
+ return new ChainSpecification(
+ new ComponentId("native"),
+ inheritsVespaPhases(),
+ noPhases(),
+ noSearcherReferences());
+ }
+
+ private static ChainSpecification vespaSearchChainSpecification() {
+ return new ChainSpecification(
+ new ComponentId("vespa"),
+ inherits(nativeSearchChainSpecification().componentId),
+ noPhases(),
+ noSearcherReferences());
+ }
+
+
+ private static ChainSpecification vespaPhasesSpecification() {
+ return new ChainSpecification(
+ new ComponentId("vespaPhases"),
+ new ChainSpecification.Inheritance(null, null),
+ PhasesCreator.linearPhases(
+ PhaseNames.RAW_QUERY,
+ PhaseNames.TRANSFORMED_QUERY,
+ PhaseNames.BLENDED_RESULT,
+ PhaseNames.UNBLENDED_RESULT,
+ PhaseNames.BACKEND),
+ noSearcherReferences());
+ }
+
+ private static SearchChain createVespaPhases() {
+ return new SearchChain(vespaPhasesSpecification());
+ }
+
+ private static SearchChain createNative() {
+ SearchChain nativeChain = new SearchChain(nativeSearchChainSpecification());
+ addInnerSearchers(nativeChain, VespaSearchers.nativeSearcherModels);
+ return nativeChain;
+ }
+
+ private static SearchChain createVespa() {
+ SearchChain vespaChain = new SearchChain(vespaSearchChainSpecification());
+ addInnerSearchers(vespaChain, VespaSearchers.vespaSearcherModels);
+ return vespaChain;
+ }
+
+ public static void addVespaSearchChains(SearchChains searchChains) {
+ searchChains.add(
+ createVespaPhases());
+ searchChains.add(
+ createNative());
+ searchChains.add(
+ createVespa());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java
new file mode 100644
index 00000000000..f6352e9ac35
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/AccessLogBuilder.java
@@ -0,0 +1,96 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.core.AccessLogConfig;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.AccessLogComponent;
+import com.yahoo.vespa.model.container.component.AccessLogComponent.AccessLogType;
+import org.w3c.dom.Element;
+
+import java.util.Optional;
+
+import static com.yahoo.collections.CollectionUtil.firstMatching;
+import static com.yahoo.config.model.builder.xml.XmlHelper.getOptionalAttribute;
+
+import static com.yahoo.config.model.builder.xml.XmlHelper.nullIfEmpty;
+
+/**
+ * @author tonytv
+ */
+public class AccessLogBuilder {
+ /*
+ * Valid values for the type attribute in services.xml
+ * Must be kept in sync with containercluster.rnc:AccessLog
+ */
+ private enum AccessLogTypeLiteral {
+ VESPA("vespa"),
+ YAPACHE("yapache"),
+ DISABLED("disabled");
+
+ final String attributeValue;
+
+ AccessLogTypeLiteral(String attributeValue) {
+ this.attributeValue = attributeValue;
+ }
+
+ static AccessLogTypeLiteral fromAttributeValue(String value) {
+ return firstMatching(
+ AccessLogTypeLiteral.values(),
+ typeLiteral -> typeLiteral.attributeValue.equals(value)).get();
+ }
+ }
+
+ private static class DomBuilder extends VespaDomBuilder.DomConfigProducerBuilder<AccessLogComponent> {
+ private final AccessLogType accessLogType;
+
+ public DomBuilder(AccessLogType accessLogType) {
+ this.accessLogType = accessLogType;
+ }
+
+ @Override
+ protected AccessLogComponent doBuild(AbstractConfigProducer ancestor, Element spec) {
+ return new AccessLogComponent(
+ accessLogType,
+ fileNamePattern(spec),
+ rotationInterval(spec),
+ rotationScheme(spec),
+ symlinkName(spec));
+ }
+
+ private String symlinkName(Element spec) {
+ return nullIfEmpty(spec.getAttribute("symlinkName"));
+ }
+
+ private AccessLogConfig.FileHandler.RotateScheme.Enum rotationScheme(Element spec) {
+ return AccessLogComponent.rotateScheme(nullIfEmpty(spec.getAttribute("rotationScheme")));
+ }
+
+ private String rotationInterval(Element spec) {
+ return nullIfEmpty(spec.getAttribute("rotationInterval"));
+ }
+
+ private String fileNamePattern(Element spec) {
+ return nullIfEmpty(spec.getAttribute("fileNamePattern"));
+ }
+ }
+
+ public static Optional<AccessLogComponent> buildIfNotDisabled(ContainerCluster cluster, Element accessLogSpec) {
+ AccessLogTypeLiteral typeLiteral =
+ getOptionalAttribute(accessLogSpec, "type").
+ map(AccessLogTypeLiteral::fromAttributeValue).
+ orElse(AccessLogTypeLiteral.VESPA);
+
+ switch (typeLiteral) {
+ case DISABLED:
+ return Optional.empty();
+ case VESPA:
+ return Optional.of(new DomBuilder(AccessLogType.queryAccessLog).build(cluster, accessLogSpec));
+ case YAPACHE:
+ return Optional.of(new DomBuilder(AccessLogType.yApacheAccessLog).build(cluster, accessLogSpec));
+ default:
+ throw new InconsistentSchemaAndCodeError();
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilder.java
new file mode 100644
index 00000000000..544f4f93c69
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilder.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.processing.handler.ProcessingHandler;
+import com.yahoo.search.handler.SearchHandler;
+import org.w3c.dom.Element;
+
+import java.util.*;
+
+/**
+ * This object builds a bundle instantiation spec from an XML element.
+ *
+ * @author gjoranv
+ */
+public class BundleInstantiationSpecificationBuilder {
+
+ public static BundleInstantiationSpecification build(Element spec, boolean legacyMode) {
+ ComponentSpecification id = XmlHelper.getIdRef(spec);
+ ComponentSpecification classId = getComponentSpecification(spec, "class");
+ ComponentSpecification bundle = getComponentSpecification(spec, "bundle");
+
+ BundleInstantiationSpecification instSpec = new BundleInstantiationSpecification(id, classId, bundle);
+ if ( ! legacyMode) // TODO: Remove?
+ validate(instSpec);
+
+ return bundle == null ? setBundleForKnownClass(instSpec) : instSpec;
+ }
+
+ private static BundleInstantiationSpecification setBundleForKnownClass(BundleInstantiationSpecification spec) {
+ return BundleMapper.getBundle(spec.getClassName()).
+ map(spec::inBundle).
+ orElse(spec);
+ }
+
+
+ private static void validate(BundleInstantiationSpecification instSpec) {
+ List<String> forbiddenClasses = Arrays.asList(
+ "com.yahoo.search.handler.SearchHandler",
+ "com.yahoo.processing.handler.ProcessingHandler");
+
+ for (String forbiddenClass: forbiddenClasses) {
+ if (forbiddenClass.equals(instSpec.getClassName())) {
+ throw new RuntimeException("Setting up " + forbiddenClass + " manually is not supported.");
+ }
+ }
+ }
+
+ //null if missing
+ private static ComponentSpecification getComponentSpecification(Element spec, String attributeName) {
+ return (spec.hasAttribute(attributeName)) ?
+ new ComponentSpecification(spec.getAttribute(attributeName)) :
+ null;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleMapper.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleMapper.java
new file mode 100644
index 00000000000..e09d47e7bc1
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/BundleMapper.java
@@ -0,0 +1,132 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author gjoranv
+ * @author lulf
+ * @since 5.45
+ */
+public class BundleMapper {
+
+ public static final Path LIBRARY_PATH = Paths.get(Defaults.getDefaults().vespaHome() + "lib/jars");
+ public static final String searchAndDocprocBundle = "container-search-and-docproc";
+
+ private static final Map<String, String> bundleFromClass;
+ private static final Map<String, Path> bundleFileFromClass;
+
+ public static Optional<String> getBundle(String className) {
+ return Optional.ofNullable(bundleFromClass.get(className));
+ }
+
+ public static Optional<Path> getBundlePath(String className) {
+ return Optional.ofNullable(absoluteBundlePath(bundleFileFromClass.get(className)));
+ }
+
+ public static Path absoluteBundlePath(Path fileName) {
+ if (fileName == null) return null;
+ return LIBRARY_PATH.resolve(fileName);
+ }
+
+ /**
+ * TODO: This is a temporary hack to ensure that users can use our internal components without
+ * specifying the bundle in which the components reside. Ideally, this information
+ * should be generated during vespamodel build time.
+ *
+ * The container_maven_plugin has much of the logic in place, but needs to be extended.
+ */
+ static {
+ bundleFromClass = new HashMap<>();
+ bundleFileFromClass = new HashMap<>();
+
+ bundleFromClass.put("com.yahoo.docproc.AbstractConcreteDocumentFactory", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.docproc.DocumentProcessor", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.docproc.SimpleDocumentProcessor", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.docproc.util.JoinerDocumentProcessor", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.docproc.util.SplitterDocumentProcessor", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.example.TimingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.language.simple.SimpleLinguistics", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.cluster.ClusterSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.fastsearch.FastSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.fastsearch.VespaBackEndSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.CJKSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.CollapsePhraseSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.IndexCombinatorSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.LiteralBoostSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.NoRankingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.NonPhrasingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.NormalizingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.PhrasingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.RecallSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.querytransform.StemmingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.BlendingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.CachingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.DocumentSourceSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.FieldCollapsingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.FillSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.JSONDebugSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.JuniperSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.KeyValueSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.MultipleResultsSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.PosSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.QuerySnapshotSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.QueryValidatingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.QuotingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.searcher.ValidateSortingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.semantics.SemanticSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.statistics.StatisticsSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.prelude.templates.SearchRendererAdaptor", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.Searcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.cluster.ClusterSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.cluster.PingableSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.FederationSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.ForwardingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.http.ConfiguredHTTPClientSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.http.ConfiguredHTTPProviderSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.http.HTTPClientSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.http.HTTPProviderSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.http.HTTPSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.news.NewsSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.federation.vespa.VespaSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.grouping.GroupingQueryParser", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.grouping.GroupingValidator", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.grouping.vespa.GroupingExecutor", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.handler.SearchWithRendererHandler", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.pagetemplates.PageTemplate", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.pagetemplates.PageTemplateSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.pagetemplates.engine.Resolver", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.pagetemplates.engine.resolvers.DeterministicResolver", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.pagetemplates.engine.resolvers.RandomResolver", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.pagetemplates.model.Renderer", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.pagetemplates.model.Renderer", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.query.rewrite.QueryRewriteSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.query.rewrite.SearchChainDispatcherSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.query.rewrite.rewriters.GenericExpansionRewriter", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.query.rewrite.rewriters.MisspellRewriter", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.query.rewrite.rewriters.NameRewriter", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.querytransform.AllLowercasingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.querytransform.DefaultPositionSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.querytransform.LegacyCombinator", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.querytransform.LowercasingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.querytransform.NGramSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.querytransform.QueryCombinator", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.querytransform.VespaLowercasingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.rendering.Renderer", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.rendering.SectionedRenderer", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.searchchain.ForkingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.searchchain.example.ExampleSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.searchers.CacheControlSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.statistics.PeakQpsSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.search.statistics.TimingSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.vespa.streamingvisitors.MetricsSearcher", searchAndDocprocBundle);
+ bundleFromClass.put("com.yahoo.vespa.streamingvisitors.VdsStreamingSearcher", searchAndDocprocBundle);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java
new file mode 100644
index 00000000000..a82291cd159
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilder.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.application.Xml;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.application.api.ApplicationPackage;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.configserver.option.CloudConfigOptions;
+import com.yahoo.vespa.model.container.configserver.ConfigserverCluster;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Builds the config model for the standalone config server.
+ *
+ * @author lulf
+ * @since 5.16
+ */
+public class ConfigServerContainerModelBuilder extends ContainerModelBuilder {
+
+ private final CloudConfigOptions options;
+ private final static String HOSTED_VESPA_INCLUDE_DIR = "hosted-vespa";
+
+ public ConfigServerContainerModelBuilder(CloudConfigOptions options) {
+ super(true, Networking.enable);
+ this.options = options;
+ }
+
+
+ @Override
+ public void doBuild(ContainerModel model, Element spec, ConfigModelContext modelContext) {
+ ApplicationPackage app = modelContext.getDeployState().getApplicationPackage();
+ if (!app.getFiles(Path.fromString(HOSTED_VESPA_INCLUDE_DIR), ".xml").isEmpty()) {
+ app.validateIncludeDir(HOSTED_VESPA_INCLUDE_DIR);
+ List<Element> configModelElements = Xml.allElemsFromPath(app, HOSTED_VESPA_INCLUDE_DIR);
+ mergeInto(spec, configModelElements);
+ }
+
+ ConfigserverCluster cluster = new ConfigserverCluster(modelContext.getParentProducer(), "configserver", options);
+ super.doBuild(model, spec, modelContext.modifyParent(cluster));
+ cluster.setContainerCluster(model.getCluster());
+ }
+
+ private void mergeInto(Element destination, List<Element> configModelElements) {
+ for (Element jdiscElement: configModelElements) {
+ for (Node child = jdiscElement.getFirstChild(); child != null; child = child.getNextSibling()) {
+ Node copiedNode = destination.getOwnerDocument().importNode(child, true);
+ destination.appendChild(copiedNode);
+ }
+ }
+ }
+
+ @Override
+ protected void addDefaultComponents(ContainerCluster containerCluster) {
+ // To avoid search specific stuff.
+ }
+
+ @Override
+ protected void addDefaultHandlers(ContainerCluster containerCluster) {
+ addDefaultHandlersExceptStatus(containerCluster);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
new file mode 100644
index 00000000000..a8055afeea8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java
@@ -0,0 +1,576 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.component.Version;
+import com.yahoo.config.application.Xml;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.IncludeDirs;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.provision.Capacity;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.container.jdisc.config.MetricDefaultsConfig;
+import com.yahoo.search.rendering.RendererRegistry;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.builder.xml.dom.DomClientProviderBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.DomComponentBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.DomFilterBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.DomHandlerBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.NodesSpecification;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.ServletBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.docproc.DomDocprocChainsBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.processing.DomProcessingBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.chains.search.DomSearchChainsBuilder;
+import com.yahoo.vespa.model.clients.ContainerDocumentApi;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.FileStatusHandlerComponent;
+import com.yahoo.vespa.model.container.component.chain.ProcessingHandler;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.container.docproc.DocprocChains;
+import com.yahoo.vespa.model.container.http.Http;
+import com.yahoo.vespa.model.container.http.xml.HttpBuilder;
+import com.yahoo.vespa.model.container.jersey.xml.RestApiBuilder;
+import com.yahoo.vespa.model.container.processing.ProcessingChains;
+import com.yahoo.vespa.model.container.search.ContainerSearch;
+import com.yahoo.vespa.model.container.search.PageTemplates;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import com.yahoo.vespa.model.container.search.SemanticRules;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import com.yahoo.vespa.model.container.xml.document.DocumentFactoryBuilder;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * @author tonytv
+ */
+public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> {
+
+ /**
+ * Default path to vip status file for container in Hosted Vespa.
+ */
+ static final String HOSTED_VESPA_STATUS_FILE = Defaults.getDefaults().vespaHome() + "var/mediasearch/oor/status.html";
+ /**
+ * Path to vip status file for container in Hosted Vespa. Only used if set, else use HOSTED_VESPA_STATUS_FILE
+ */
+ static final String HOSTED_VESPA_STATUS_FILE_YINST_SETTING = "cloudconfig_server__tenant_vip_status_file";
+
+ public enum Networking { disable, enable }
+
+ private ApplicationPackage app;
+ private final boolean standaloneBuilder;
+ private final Networking networking;
+ protected DeployLogger log;
+
+ public static final List<ConfigModelId> configModelIds =
+ ImmutableList.of(ConfigModelId.fromName("container"), ConfigModelId.fromName("jdisc"));
+
+ private static final String xmlRendererId = RendererRegistry.xmlRendererId.getName();
+ private static final String jsonRendererId = RendererRegistry.jsonRendererId.getName();
+
+ public ContainerModelBuilder(boolean standaloneBuilder, Networking networking) {
+ super(ContainerModel.class);
+ this.standaloneBuilder = standaloneBuilder;
+ this.networking = networking;
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return configModelIds;
+ }
+
+ @Override
+ public void doBuild(ContainerModel model, Element spec, ConfigModelContext modelContext) {
+ app = modelContext.getApplicationPackage();
+ checkVersion(spec);
+
+ this.log = modelContext.getDeployLogger();
+
+ ContainerCluster cluster = createContainerCluster(spec, modelContext);
+ addClusterContent(cluster, spec, modelContext);
+ addBundlesForPlatformComponents(cluster);
+
+ model.setCluster(cluster);
+ }
+
+ protected void addBundlesForPlatformComponents(ContainerCluster cluster) {
+ for (Component<?, ?> component : cluster.getAllComponents()) {
+ String componentClass = component.model.bundleInstantiationSpec.getClassName();
+ BundleMapper.getBundlePath(componentClass).
+ ifPresent(cluster::addPlatformBundle);
+ }
+ }
+
+ private ContainerCluster createContainerCluster(Element spec, final ConfigModelContext modelContext) {
+ return new VespaDomBuilder.DomConfigProducerBuilder<ContainerCluster>() {
+ @Override
+ protected ContainerCluster doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ return new ContainerCluster(ancestor, modelContext.getProducerId(), modelContext.getProducerId());
+ }
+ }.build(modelContext.getParentProducer(), spec);
+ }
+
+ private void addClusterContent(ContainerCluster cluster, Element spec, ConfigModelContext modelContext) {
+ DocumentFactoryBuilder.buildDocumentFactories(cluster, spec);
+
+ addConfiguredComponents(cluster, spec);
+ addHandlers(cluster, spec);
+
+ addHttp(spec, cluster);
+ addRestApis(spec, cluster);
+ addServlets(spec, cluster);
+ addProcessing(spec, cluster);
+ addSearch(spec, cluster, modelContext.getDeployState().getQueryProfiles(), modelContext.getDeployState().getSemanticRules());
+ addDocproc(spec, cluster);
+ addDocumentApi(spec, cluster); //NOTE: Document API must be set up _after_ search!
+
+ addAccessLogs(cluster, spec);
+ addRoutingAliases(cluster, spec);
+ addNodes(cluster, spec);
+
+ addClientProviders(spec, cluster);
+ addServerProviders(spec, cluster);
+ addLegacyFilters(spec, cluster);
+
+ addDefaultHandlers(cluster);
+ addStatusHandlers(cluster, modelContext);
+ addDefaultComponents(cluster);
+ setDefaultMetricConsumerFactory(cluster);
+
+ //TODO: overview handler, see DomQrserverClusterBuilder
+ //TODO: cache options.
+ }
+
+ private void addRoutingAliases(ContainerCluster cluster, Element spec) {
+ Element aliases = XML.getChild(spec, "aliases");
+ for (Element alias : XML.getChildren(aliases, "service-alias")) {
+ cluster.serviceAliases().add(XML.getValue(alias));
+ }
+ for (Element alias : XML.getChildren(aliases, "endpoint-alias")) {
+ cluster.endpointAliases().add(XML.getValue(alias));
+ }
+ }
+
+ private void addConfiguredComponents(ContainerCluster cluster, Element spec) {
+ for (Element components : XML.getChildren(spec, "components")) {
+ addIncludes(components);
+ addConfiguredComponents(cluster, components, "component");
+ }
+ addConfiguredComponents(cluster, spec, "component");
+ }
+
+ protected void addDefaultComponents(ContainerCluster cluster) {
+ }
+
+ protected void setDefaultMetricConsumerFactory(ContainerCluster cluster) {
+ cluster.setDefaultMetricConsumerFactory(MetricDefaultsConfig.Factory.Enum.STATE_MONITOR);
+ }
+
+ protected void addDefaultHandlers(ContainerCluster cluster) {
+ addDefaultHandlersExceptStatus(cluster);
+ }
+
+ protected void addStatusHandlers(ContainerCluster cluster, ConfigModelContext configModelContext) {
+ if (configModelContext.getDeployState().isHostedVespa()) {
+ String name = "status.html";
+ Optional<String> statusFile = Optional.ofNullable(System.getenv(HOSTED_VESPA_STATUS_FILE_YINST_SETTING));
+ cluster.addComponent(
+ new FileStatusHandlerComponent(name + "-status-handler", statusFile.orElse(HOSTED_VESPA_STATUS_FILE),
+ "http://*/" + name, "https://*/" + name));
+ } else {
+ cluster.addVipHandler();
+ }
+ }
+
+ /**
+ * Intended for use by legacy builders only.
+ * Will be called during building when using ContainerModelBuilder.
+ */
+ public static void addDefaultHandler_legacyBuilder(ContainerCluster cluster) {
+ addDefaultHandlersExceptStatus(cluster);
+ cluster.addVipHandler();
+ }
+
+ protected static void addDefaultHandlersExceptStatus(ContainerCluster cluster) {
+ cluster.addDefaultRootHandler();
+ cluster.addMetricStateHandler();
+ cluster.addApplicationStatusHandler();
+ cluster.addStatisticsHandler();
+ }
+
+ private void addClientProviders(Element spec, ContainerCluster cluster) {
+ for (Element clientSpec: XML.getChildren(spec, "client")) {
+ cluster.addComponent(new DomClientProviderBuilder().build(cluster, clientSpec));
+ }
+ }
+
+ private void addServerProviders(Element spec, ContainerCluster cluster) {
+ addConfiguredComponents(cluster, spec, "server");
+ }
+
+ private void addLegacyFilters(Element spec, ContainerCluster cluster) {
+ for (Component component : buildLegacyFilters(cluster, spec)) {
+ cluster.addComponent(component);
+ }
+ }
+
+ private List<Component> buildLegacyFilters(AbstractConfigProducer ancestor,
+ Element spec) {
+ List<Component> components = new ArrayList<>();
+
+ for (Element node : XML.getChildren(spec, "filter")) {
+ components.add(new DomFilterBuilder().build(ancestor, node));
+ }
+ return components;
+ }
+
+ protected void addAccessLogs(ContainerCluster cluster, Element spec) {
+ List<Element> accessLogElements = getAccessLogElements(spec);
+
+ for (Element accessLog : accessLogElements) {
+ AccessLogBuilder.buildIfNotDisabled(cluster, accessLog).ifPresent(cluster::addComponent);
+ }
+
+ if (accessLogElements.isEmpty() && cluster.getSearch() != null)
+ cluster.addDefaultSearchAccessLog();
+ }
+
+ protected final List<Element> getAccessLogElements(Element spec) {
+ return XML.getChildren(spec, "accesslog");
+ }
+
+
+ protected void addHttp(Element spec, ContainerCluster cluster) {
+ Element httpElement = XML.getChild(spec, "http");
+ if (httpElement != null) {
+ cluster.setHttp(buildHttp(cluster, httpElement));
+ }
+ }
+
+ private Http buildHttp(ContainerCluster cluster, Element httpElement) {
+ Http http = new HttpBuilder().build(cluster, httpElement);
+
+ if (networking == Networking.disable)
+ http.removeAllServers();
+
+ return http;
+ }
+
+ protected void addRestApis(Element spec, ContainerCluster cluster) {
+ for (Element restApiElem : XML.getChildren(spec, "rest-api")) {
+ cluster.addRestApi(
+ new RestApiBuilder().build(cluster, restApiElem));
+ }
+ }
+
+ private void addServlets(Element spec, ContainerCluster cluster) {
+ for (Element servletElem : XML.getChildren(spec, "servlet")) {
+ cluster.addServlet(
+ new ServletBuilder().build(cluster, servletElem));
+ }
+ }
+
+ private void addDocumentApi(Element spec, ContainerCluster cluster) {
+ ContainerDocumentApi containerDocumentApi = buildDocumentApi(cluster, spec);
+ if (containerDocumentApi != null) {
+ cluster.setDocumentApi(containerDocumentApi);
+ }
+ }
+
+ private void addDocproc(Element spec, ContainerCluster cluster) {
+ ContainerDocproc containerDocproc = buildDocproc(cluster, spec);
+ if (containerDocproc != null) {
+ cluster.setDocproc(containerDocproc);
+
+ ContainerDocproc.Options docprocOptions = containerDocproc.options;
+ cluster.setMbusParams(new ContainerCluster.MbusParams(
+ docprocOptions.maxConcurrentFactor, docprocOptions.documentExpansionFactor, docprocOptions.containerCoreMemory));
+ }
+ }
+
+ private void addSearch(Element spec, ContainerCluster cluster, QueryProfiles queryProfiles, SemanticRules semanticRules) {
+ Element searchElement = XML.getChild(spec, "search");
+ if (searchElement != null) {
+ addIncludes(searchElement);
+ cluster.setSearch(buildSearch(cluster, searchElement, queryProfiles, semanticRules));
+
+ addSearchHandler(cluster, searchElement);
+ validateAndAddConfiguredComponents(cluster, searchElement, "renderer", ContainerModelBuilder::validateRendererElement);
+ }
+ }
+
+ private void addProcessing(Element spec, ContainerCluster cluster) {
+ Element processingElement = XML.getChild(spec, "processing");
+ if (processingElement != null) {
+ addIncludes(processingElement);
+ cluster.setProcessingChains(new DomProcessingBuilder(null).build(cluster, processingElement),
+ serverBindings(processingElement, ProcessingChains.defaultBindings));
+ validateAndAddConfiguredComponents(cluster, processingElement, "renderer", ContainerModelBuilder::validateRendererElement);
+ }
+ }
+
+ private ContainerSearch buildSearch(ContainerCluster containerCluster, Element producerSpec,
+ QueryProfiles queryProfiles, SemanticRules semanticRules) {
+ SearchChains searchChains = new DomSearchChainsBuilder(null, false).build(containerCluster, producerSpec);
+
+ ContainerSearch containerSearch = new ContainerSearch(containerCluster, searchChains, new ContainerSearch.Options());
+
+ applyApplicationPackageDirectoryConfigs(containerCluster.getRoot().getDeployState().getApplicationPackage(), containerSearch);
+ containerSearch.setQueryProfiles(queryProfiles);
+ containerSearch.setSemanticRules(semanticRules);
+
+ return containerSearch;
+ }
+
+ private void applyApplicationPackageDirectoryConfigs(ApplicationPackage applicationPackage,ContainerSearch containerSearch) {
+ PageTemplates.validate(applicationPackage);
+ containerSearch.setPageTemplates(PageTemplates.create(applicationPackage));
+ }
+
+ private void addHandlers(ContainerCluster cluster, Element spec) {
+ for (Element component: XML.getChildren(spec, "handler")) {
+ cluster.addComponent(
+ new DomHandlerBuilder().build(cluster, component));
+ }
+ }
+
+ private void checkVersion(Element spec) {
+ String version = spec.getAttribute("version");
+
+ if (!Version.fromString(version).equals(new Version(1))) {
+ throw new RuntimeException("Expected container version to be 1.0, but got " + version);
+ }
+ }
+
+ private void addNodes(ContainerCluster cluster, Element spec) {
+ if (standaloneBuilder) {
+ addStandaloneNode(cluster);
+ } else {
+ addNodesFromXml(cluster, spec);
+ }
+ }
+
+ private void addStandaloneNode(ContainerCluster cluster) {
+ Container container = new Container(cluster, "standalone");
+ cluster.addContainers(Collections.singleton(container));
+ }
+
+ private void addNodesFromXml(ContainerCluster cluster, Element spec) {
+ Element nodesElement = XML.getChild(spec, "nodes");
+ if (nodesElement == null) { // default single node on localhost
+ Container container = new Container(cluster, "container.0");
+ HostResource host = allocateSingleNodeHost(cluster, log);
+ container.setHostResource(host);
+ if ( ! container.isInitialized() ) // TODO: Fold this into initService
+ container.initService();
+ cluster.addContainers(Collections.singleton(container));
+ }
+ else {
+ String defaultJvmArgs = nodesElement.getAttribute(VespaDomBuilder.JVMARGS_ATTRIB_NAME);
+ String defaultPreLoad = null;
+ if (nodesElement.hasAttribute(VespaDomBuilder.PRELOAD_ATTRIB_NAME)) {
+ defaultPreLoad = nodesElement.getAttribute(VespaDomBuilder.PRELOAD_ATTRIB_NAME);
+ }
+ boolean useCpuSocketAffinity = false;
+ if (nodesElement.hasAttribute(VespaDomBuilder.CPU_SOCKET_AFFINITY_ATTRIB_NAME)) {
+ useCpuSocketAffinity = Boolean.parseBoolean(nodesElement.getAttribute(VespaDomBuilder.CPU_SOCKET_AFFINITY_ATTRIB_NAME));
+ }
+ List<Container> result = new ArrayList<>();
+ result.addAll(createNodesFromXmlNodeCount(cluster, nodesElement));
+ addNodesFromXmlNodeList(cluster, spec, nodesElement, result);
+ applyDefaultJvmArgs(result, defaultJvmArgs);
+ applyRoutingAliasProperties(result, cluster);
+ if (defaultPreLoad != null) {
+ applyDefaultPreload(result, defaultPreLoad);
+ }
+ if (useCpuSocketAffinity) {
+ AbstractService.distributeCpuSocketAffinity(result);
+ }
+
+ cluster.addContainers(result);
+ }
+ }
+
+ private void applyRoutingAliasProperties(List<Container> result, ContainerCluster cluster) {
+ if (!cluster.serviceAliases().isEmpty()) {
+ result.forEach(container -> {
+ container.setProp("servicealiases", cluster.serviceAliases().stream().collect(Collectors.joining(",")));
+ });
+ }
+ if (!cluster.endpointAliases().isEmpty()) {
+ result.forEach(container -> {
+ container.setProp("endpointaliases", cluster.endpointAliases().stream().collect(Collectors.joining(",")));
+ });
+ }
+ }
+
+ private HostResource allocateSingleNodeHost(ContainerCluster cluster, DeployLogger logger) {
+ if (cluster.isHostedVespa()) {
+ ClusterSpec clusterSpec = ClusterSpec.from(ClusterSpec.Type.container, ClusterSpec.Id.from(cluster.getName()), Optional.empty());
+ return cluster.getHostSystem().allocateHosts(clusterSpec, Capacity.fromNodeCount(1), 1, logger).keySet().iterator().next();
+ } else {
+ return cluster.getHostSystem().getHost(Container.SINGLENODE_CONTAINER_SERVICESPEC);
+ }
+ }
+
+ private void addNodesFromXmlNodeList(ContainerCluster cluster,
+ Element spec, Element nodesElement, List<Container> result) {
+ int nodeCount = 0;
+ for (Element nodeElem: XML.getChildren(nodesElement, "node")) {
+ Container container = new ContainerServiceBuilder("container." + nodeCount).build(cluster, nodeElem);
+ result.add(container);
+ ++nodeCount;
+ }
+ }
+
+ private List<Container> createNodesFromXmlNodeCount(ContainerCluster cluster, Element nodesElement) {
+ List<Container> result = new ArrayList<>();
+ if (nodesElement.hasAttribute("count")) {
+ NodesSpecification nodesSpecification = NodesSpecification.from(new ModelElement(nodesElement));
+ Map<HostResource, ClusterMembership> hosts = nodesSpecification.provision(cluster.getRoot().getHostSystem(), ClusterSpec.Type.container, ClusterSpec.Id.from(cluster.getName()), Optional.empty(), log);
+ for (Map.Entry<HostResource, ClusterMembership> entry : hosts.entrySet()) {
+ String id = "container." + entry.getValue().index();
+ Container container = new Container(cluster, id, entry.getValue().retired());
+ container.setHostResource(entry.getKey());
+ container.initService();
+ result.add(container);
+ }
+ }
+ return result;
+ }
+
+ private void applyDefaultJvmArgs(List<Container> containers, String defaultJvmArgs) {
+ for (Container container: containers) {
+ if (container.getJvmArgs().isEmpty())
+ container.prependJvmArgs(defaultJvmArgs);
+ }
+ }
+
+ private void applyDefaultPreload(List<Container> containers, String defaultPreLoad) {
+ for (Container container: containers) {
+ container.setPreLoad(defaultPreLoad);
+ }
+ }
+
+ private void addSearchHandler(ContainerCluster cluster, Element searchElement) {
+ ProcessingHandler<SearchChains> searchHandler = new ProcessingHandler<>(
+ cluster.getSearch().getChains(), "com.yahoo.search.handler.SearchHandler");
+
+ String[] defaultBindings = {"http://*/search/*", "https://*/search/*"};
+ for (String binding: serverBindings(searchElement, defaultBindings)) {
+ searchHandler.addServerBindings(binding);
+ }
+
+ cluster.addComponent(searchHandler);
+ }
+
+ private String[] serverBindings(Element searchElement, String... defaultBindings) {
+ List<Element> bindings = XML.getChildren(searchElement, "binding");
+ if (bindings.isEmpty())
+ return defaultBindings;
+
+ return toBindingList(bindings);
+ }
+
+ private String[] toBindingList(List<Element> bindingElements) {
+ List<String> result = new ArrayList<>();
+
+ for (Element element: bindingElements) {
+ String text = element.getTextContent().trim();
+ if (!text.isEmpty())
+ result.add(text);
+ }
+
+ return result.toArray(new String[result.size()]);
+ }
+
+ private ContainerDocumentApi buildDocumentApi(ContainerCluster cluster, Element spec) {
+ Element documentApiElement = XML.getChild(spec, "document-api");
+ if (documentApiElement == null) {
+ return null;
+ }
+
+ ContainerDocumentApi.Options documentApiOptions = DocumentApiOptionsBuilder.build(documentApiElement);
+ return new ContainerDocumentApi(cluster, documentApiOptions);
+ }
+
+ private ContainerDocproc buildDocproc(ContainerCluster cluster, Element spec) {
+ Element docprocElement = XML.getChild(spec, "document-processing");
+ if (docprocElement == null)
+ return null;
+
+ addIncludes(docprocElement);
+ DocprocChains chains = new DomDocprocChainsBuilder(null, false).build(cluster, docprocElement);
+
+ ContainerDocproc.Options docprocOptions = DocprocOptionsBuilder.build(docprocElement);
+ return new ContainerDocproc(cluster, chains, docprocOptions, !standaloneBuilder);
+ }
+
+ private void addIncludes(Element parentElement) {
+ List<Element> includes = XML.getChildren(parentElement, IncludeDirs.INCLUDE);
+ if (includes == null || includes.isEmpty()) {
+ return;
+ }
+ if (app == null) {
+ throw new IllegalArgumentException("Element <include> given in XML config, but no application package given.");
+ }
+ for (Element include : includes) {
+ addInclude(parentElement, include);
+ }
+ }
+
+ private void addInclude(Element parentElement, Element include) {
+ String dirName = include.getAttribute(IncludeDirs.DIR);
+ app.validateIncludeDir(dirName);
+
+ List<Element> includedFiles = Xml.allElemsFromPath(app, dirName);
+ for (Element includedFile : includedFiles) {
+ List<Element> includedSubElements = XML.getChildren(includedFile);
+ for (Element includedSubElement : includedSubElements) {
+ Node copiedNode = parentElement.getOwnerDocument().importNode(includedSubElement, true);
+ parentElement.appendChild(copiedNode);
+ }
+ }
+ }
+
+ public static void addConfiguredComponents(ContainerCluster cluster, Element spec, String componentName) {
+ for (Element node : XML.getChildren(spec, componentName)) {
+ cluster.addComponent(new DomComponentBuilder().build(cluster, node));
+ }
+ }
+
+ public static void validateAndAddConfiguredComponents(ContainerCluster cluster, Element spec, String componentName, Consumer<Element> elementValidator) {
+ for (Element node : XML.getChildren(spec, componentName)) {
+ elementValidator.accept(node); // throws exception here if something is wrong
+ cluster.addComponent(new DomComponentBuilder().build(cluster, node));
+ }
+ }
+
+ /**
+ * Disallow renderers named "DefaultRenderer" or "JsonRenderer"
+ */
+ private static void validateRendererElement(Element element) {
+ String idAttr = element.getAttribute("id");
+
+ if (idAttr.equals(xmlRendererId) || idAttr.equals(jsonRendererId)) {
+ throw new IllegalArgumentException(String.format("Renderer id %s is reserved for internal use", idAttr));
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java
new file mode 100644
index 00000000000..785ab1f7504
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerServiceBuilder.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.ConfigModelUtils;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.Container;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * @author tonytv
+ */
+public class ContainerServiceBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Container> {
+
+ private final String id;
+
+ public ContainerServiceBuilder(String id) {
+ this.id = id;
+ }
+
+ @Override
+ protected Container doBuild(AbstractConfigProducer parent, Element nodeElem) {
+ return new Container(parent, id, readServerPortOverrides(nodeElem));
+ }
+
+ private List<Container.PortOverride> readServerPortOverrides(Element spec) {
+ List<Container.PortOverride> portOverrides = new ArrayList<>();
+
+ for (Element serverPort: XML.getChildren(spec, "server-port")) {
+ ComponentSpecification serverId = XmlHelper.getIdRef(serverPort);
+ int port = Integer.parseInt(serverPort.getAttribute("port"));
+
+ portOverrides.add(new Container.PortOverride(serverId, port));
+ }
+
+ return portOverrides;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocprocOptionsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocprocOptionsBuilder.java
new file mode 100644
index 00000000000..0b13187bd58
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocprocOptionsBuilder.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import org.w3c.dom.Element;
+
+/**
+ * Extracted from DomDocProcClusterBuilder
+ */
+public class DocprocOptionsBuilder {
+ public static ContainerDocproc.Options build(Element spec) {
+ return new ContainerDocproc.Options(
+ getCompression(spec),
+ getMaxMessagesInQueue(spec),
+ getSizeInMegabytes(spec.getAttribute("maxqueuebytesize")),
+ getTime(spec.getAttribute("maxqueuewait")),
+ getFactor(spec.getAttribute("maxconcurrentfactor")),
+ getFactor(spec.getAttribute("documentexpansionfactor")),
+ getInt(spec.getAttribute("containercorememory")));
+ }
+
+ private static Integer getInt(String integer) {
+ return integer == null || integer.trim().isEmpty() ?
+ null:
+ Integer.parseInt(integer);
+ }
+
+ private static boolean getCompression(Element spec) {
+ return (spec.hasAttribute("compressdocuments") && spec.getAttribute("compressdocuments").equals("true"));
+ }
+
+ private static Double getFactor(String factor) {
+ return factor == null || factor.trim().isEmpty() ?
+ null :
+ Double.parseDouble(factor);
+ }
+
+
+ private static Integer getMaxMessagesInQueue(Element spec) {
+ // get max queue size (number of messages), if set
+ Integer maxMessagesInQueue = null;
+ if (spec.hasAttribute("maxmessagesinqueue")) {
+ maxMessagesInQueue = Integer.valueOf(spec.getAttribute("maxmessagesinqueue"));
+ }
+ return maxMessagesInQueue;
+ }
+
+ private static Integer getSizeInMegabytes(String size) {
+ if (size == null) {
+ return null;
+ }
+ size = size.trim();
+ if (size.isEmpty()) {
+ return null;
+ }
+
+ Integer megabyteSize;
+ if (size.endsWith("m")) {
+ size = size.substring(0, size.length() - 1);
+ megabyteSize = Integer.parseInt(size);
+ } else if (size.endsWith("g")) {
+ size = size.substring(0, size.length() - 1);
+ megabyteSize = Integer.parseInt(size) * 1024;
+ } else {
+ throw new IllegalArgumentException("Heap sizes for docproc must be set to Xm or Xg, where X is an integer specifying megabytes or gigabytes, respectively.");
+ }
+ return megabyteSize;
+ }
+
+ private static Integer getTime(String intStr) {
+ if (intStr == null) {
+ return null;
+ }
+ intStr = intStr.trim();
+ if (intStr.isEmpty()) {
+ return null;
+ }
+
+ return 1000 * (int)Double.parseDouble(intStr);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocumentApiOptionsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocumentApiOptionsBuilder.java
new file mode 100644
index 00000000000..df2090db166
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/DocumentApiOptionsBuilder.java
@@ -0,0 +1,119 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.clients.ContainerDocumentApi;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.11
+ */
+public class DocumentApiOptionsBuilder {
+ private static final Logger log = Logger.getLogger(DocumentApiOptionsBuilder.class.getName());
+ private static final String[] DEFAULT_BINDINGS = {"http://*/", "https://*/"};
+
+ public static ContainerDocumentApi.Options build(Element spec) {
+ return new ContainerDocumentApi.Options(
+ getBindings(spec),
+ getAbortOnDocumentError(spec),
+ getRoute(spec),
+ getMaxPendingDocs(spec),
+ getMaxPendingBytes(spec),
+ getRetryEnabled(spec),
+ getRetryDelay(spec),
+ getTimeout(spec),
+ getTracelevel(spec),
+ getMbusPort(spec),
+ getDocprocChain(spec));
+ }
+
+ private static List<String> getBindings(Element spec) {
+ Collection<Element> bindingElems = XML.getChildren(spec, "binding");
+ if (bindingElems.isEmpty())
+ return Arrays.asList(DEFAULT_BINDINGS);
+
+ List<String> bindings = new ArrayList<>();
+ for (Element e :bindingElems) {
+ String binding = getBinding(e);
+ bindings.add(binding);
+ }
+ return bindings;
+ }
+
+ private static String getBinding(Element e) {
+ String binding = XML.getValue(e);
+ if (! binding.endsWith("/")) {
+ log.warning("Adding a trailing '/' to the document-api binding: " + binding + " -> " + binding + "/");
+ binding = binding + "/";
+ }
+ return binding;
+ }
+
+ private static String getCleanValue(Element spec, String name) {
+ Element elem = XML.getChild(spec, name);
+ if (elem == null) {
+ return null;
+ }
+ String value = elem.getFirstChild().getNodeValue();
+ if (value == null) {
+ return null;
+ }
+ value = value.trim();
+ return value.isEmpty() ? null : value;
+ }
+
+ private static String getDocprocChain(Element spec) {
+ return getCleanValue(spec, "docprocchain");
+ }
+
+ private static Integer getMbusPort(Element spec) {
+ String value = getCleanValue(spec, "mbusport");
+ return value == null ? null : Integer.parseInt(value);
+ }
+
+ private static Integer getTracelevel(Element spec) {
+ String value = getCleanValue(spec, "tracelevel");
+ return value == null ? null : Integer.parseInt(value);
+ }
+
+ private static Double getTimeout(Element spec) {
+ String value = getCleanValue(spec, "timeout");
+ return value == null ? null : Double.parseDouble(value);
+ }
+
+ private static Double getRetryDelay(Element spec) {
+ String value = getCleanValue(spec, "retrydelay");
+ return value == null ? null : Double.parseDouble(value);
+ }
+
+ private static Boolean getRetryEnabled(Element spec) {
+ String value = getCleanValue(spec, "retryenabled");
+ return value == null ? null : Boolean.parseBoolean(value);
+ }
+
+ private static Integer getMaxPendingBytes(Element spec) {
+ String value = getCleanValue(spec, "maxpendingbytes");
+ return value == null ? null : Integer.parseInt(value);
+ }
+
+ private static Integer getMaxPendingDocs(Element spec) {
+ String value = getCleanValue(spec, "maxpendingdocs");
+ return value == null ? null : Integer.parseInt(value);
+ }
+
+ private static String getRoute(Element spec) {
+ return getCleanValue(spec, "route");
+ }
+
+ private static Boolean getAbortOnDocumentError(Element spec) {
+ String value = getCleanValue(spec, "abortondocumenterror");
+ return value == null ? null : Boolean.parseBoolean(value);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/InconsistentSchemaAndCodeError.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/InconsistentSchemaAndCodeError.java
new file mode 100644
index 00000000000..46e7271fe9b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/InconsistentSchemaAndCodeError.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+/**
+ * @author tonytv
+ */
+public class InconsistentSchemaAndCodeError extends Error {}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilder.java
new file mode 100644
index 00000000000..f6ed6c2eb7d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilder.java
@@ -0,0 +1,172 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.container.jdisc.config.MetricDefaultsConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.AccessLogComponent;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.FileStatusHandlerComponent;
+import com.yahoo.vespa.model.container.http.ConnectorFactory;
+import com.yahoo.vespa.model.container.http.FilterChains;
+import com.yahoo.vespa.model.container.http.Http;
+import com.yahoo.vespa.model.container.http.JettyHttpServer;
+import org.w3c.dom.Element;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.yahoo.collections.CollectionUtil.first;
+import static com.yahoo.container.core.AccessLogConfig.FileHandler.RotateScheme;
+import static com.yahoo.vespa.model.container.xml.BundleMapper.absoluteBundlePath;
+
+/**
+ * @author tonytv
+ */
+public final class ManhattanContainerModelBuilder extends ContainerModelBuilder {
+
+ static final String MANHATTAN_FILE_NAME_PATTERN = Defaults.getDefaults().vespaHome() + "logs/jdisc_core/access.%Y-%m-%d-%H";
+ static final String MANHATTAN_ROTATION_INTERVAL = "0 60 ...";
+ static final RotateScheme.Enum MANHATTAN_ROTATION_SCHEME = RotateScheme.DATE;
+ static final String MANHATTAN_SYMLINK_NAME = "access";
+
+ public interface BundleFiles {
+ // TODO: move constants to the DH code base.
+ Set<Path> dhBundles = new HashSet<>(Arrays.asList(
+ Paths.get("apache_avro/avro.jar"),
+ Paths.get("apache_avro/commons-compress.jar"),
+ Paths.get("apache_avro/paranamer.jar"),
+ Paths.get("apache_avro/jackson-core-asl.jar"),
+ Paths.get("apache_avro/jackson-mapper-asl.jar"),
+ Paths.get("dh_rainbow_client_api_java.jar"),
+ Paths.get("dh_rainbow_util_batch_java.jar"),
+ Paths.get("dh_rainbow_util_java.jar")));
+ }
+
+ private final int httpPort;
+ private JettyHttpServer jettyHttpServer;
+
+ public ManhattanContainerModelBuilder(int httpPort) {
+ super(true, Networking.enable);
+ this.httpPort = httpPort;
+ }
+
+ @Override
+ protected void addBundlesForPlatformComponents(ContainerCluster cluster) {
+ super.addBundlesForPlatformComponents(cluster);
+ BundleFiles.dhBundles.forEach(
+ bundleFile -> cluster.addPlatformBundle(absoluteBundlePath(bundleFile)));
+ }
+
+ @Override
+ protected void setDefaultMetricConsumerFactory(ContainerCluster cluster) {
+ cluster.setDefaultMetricConsumerFactory(MetricDefaultsConfig.Factory.Enum.YAMAS_SCOREBOARD);
+ }
+
+ @Override
+ protected void addAccessLogs(ContainerCluster cluster, Element spec) {
+ warnIfAccessLogsDefined(spec);
+
+ checkNotNull(jettyHttpServer, "addHttp must be called first");
+ cluster.addComponent(createManhattanAccessLog());
+ }
+
+ private Component createManhattanAccessLog() {
+ return new AccessLogComponent(AccessLogComponent.AccessLogType.yApacheAccessLog,
+ MANHATTAN_FILE_NAME_PATTERN,
+ MANHATTAN_ROTATION_INTERVAL,
+ MANHATTAN_ROTATION_SCHEME,
+ MANHATTAN_SYMLINK_NAME);
+ }
+
+ private void warnIfAccessLogsDefined(Element spec) {
+ List<Element> accessLogElements = getAccessLogElements(spec);
+ if (!accessLogElements.isEmpty()) {
+ logManhattanInfo("Ignoring " + accessLogElements.size() +
+ " access log elements in services.xml, using default yapache access logging instead.");
+ }
+ }
+
+ @Override
+ protected void addDefaultHandlers(ContainerCluster cluster) {
+ addDefaultHandlersExceptStatus(cluster);
+ }
+
+ @Override
+ protected void addStatusHandlers(ContainerCluster cluster, ConfigModelContext configModelContext) {
+ addStatusHandlerForJDiscStatusPackage(cluster, "status.html"); //jdisc_status
+ addStatusHandlerForJDiscStatusPackage(cluster, "akamai"); //jdisc_akamai
+ }
+
+ private static void addStatusHandlerForJDiscStatusPackage(ContainerCluster cluster, String name) {
+ cluster.addComponent(
+ new FileStatusHandlerComponent(name + "-status-handler", Defaults.getDefaults().vespaHome() + "libexec/jdisc/" + name,
+ "http://*/" + name, "https://*/" + name));
+ }
+
+ @Override
+ protected void addHttp(Element spec, ContainerCluster cluster) {
+ super.addHttp(spec, cluster);
+ ensureHasHttp(cluster);
+ ensureOneHttpServer(cluster.getHttp());
+ }
+
+ private void ensureHasHttp(ContainerCluster cluster) {
+ if (cluster.getHttp() == null)
+ cluster.setHttp(createHttp());
+ }
+
+ private Http createHttp() {
+ Http http = new Http(Collections.<Http.Binding>emptyList());
+ http.setFilterChains(new FilterChains(http));
+ return http;
+ }
+
+ private void ensureOneHttpServer(Http http) {
+ if (http.getHttpServer() == null || http.getHttpServer().getConnectorFactories().isEmpty()) {
+ JettyHttpServer jettyHttpServer = new JettyHttpServer(new ComponentId("main-http-server"));
+ http.setHttpServer(jettyHttpServer);
+ ConnectorFactory connectorFactory = new ConnectorFactory("main-http-connector",
+ httpPort, null);
+ http.getHttpServer().addConnector(connectorFactory);
+ } else {
+ removeAllButOneConnector(http.getHttpServer());
+ ConnectorFactory connectorFactory = first(http.getHttpServer().getConnectorFactories());
+ connectorFactory.setListenPort(httpPort);
+ }
+ jettyHttpServer = http.getHttpServer();
+ }
+
+ private void removeAllButOneConnector(JettyHttpServer jettyHttpServer) {
+ int removed = 0;
+
+ if (jettyHttpServer.getConnectorFactories().size() > 1) {
+ for (int i = jettyHttpServer.getConnectorFactories().size() - 1; i > 0; i--) {
+ ConnectorFactory c = jettyHttpServer.getConnectorFactories().get(i);
+ jettyHttpServer.removeConnector(c);
+ ++removed;
+ }
+ }
+
+ if (removed > 0) {
+ logManhattanInfo("Using only the first http server " + jettyHttpServer.getConnectorFactories().get(0).getName());
+ }
+ }
+
+ private static <E> List<E> tail(List<E> list) {
+ return list.subList(1, list.size());
+ }
+
+ private void logManhattanInfo(String message) {
+ log.log(Level.INFO, "[Manhattan] " + message);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/document/DocumentFactoryBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/document/DocumentFactoryBuilder.java
new file mode 100644
index 00000000000..96ab9fd2f52
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/document/DocumentFactoryBuilder.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml.document;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import org.w3c.dom.Element;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Handles the document bindings (concrete document types). Register the concrete document factories as components.
+ *
+ * @author vegardh
+ * @since 5.1.10
+ */
+public class DocumentFactoryBuilder {
+ private static final String CONCRETE_DOC_FACTORY_CLASS = "ConcreteDocumentFactory";
+
+ public static void buildDocumentFactories(ContainerCluster cluster, Element spec) {
+ Map<String, String> types = new LinkedHashMap<>();
+ for (Element e : XML.getChildren(spec, "document")) {
+ String type = e.getAttribute("type");
+ String clazz = e.getAttribute("class");
+ // Empty pkg is forbidden in the documentgen Mojo.
+ if (clazz.indexOf('.')<0) throw new IllegalArgumentException("Malformed class for <document> binding, must be a full class with package: "+clazz);
+ String pkg = clazz.substring(0, clazz.lastIndexOf('.'));
+ String concDocFactory=pkg+"."+CONCRETE_DOC_FACTORY_CLASS;
+ String bundle = e.getAttribute("bundle");
+ Component<AbstractConfigProducer<?>, ComponentModel> component = new Component<>(
+ new ComponentModel(BundleInstantiationSpecification.getFromStrings(concDocFactory, concDocFactory, bundle)));
+ if (!cluster.getComponentsMap().containsKey(component.getComponentId())) cluster.addComponent(component);
+ types.put(type, concDocFactory);
+ }
+ cluster.concreteDocumentTypes().putAll(types);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/package-info.java
new file mode 100644
index 00000000000..2ae232adcd6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/BucketSplitting.java b/config-model/src/main/java/com/yahoo/vespa/model/content/BucketSplitting.java
new file mode 100644
index 00000000000..e320e44c237
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/BucketSplitting.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.core.StorDistributormanagerConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+/**
+ * Represents configuration for bucket splitting.
+ */
+public class BucketSplitting implements StorDistributormanagerConfig.Producer {
+ Integer maxDocuments;
+ Integer splitSize;
+ Integer minSplitCount;
+ boolean useInlineBucketSplitting;
+
+ public static class Builder {
+ public BucketSplitting build(ContentCluster cluster, ModelElement clusterElem) {
+ ModelElement tuning = clusterElem.getChild("tuning");
+ if (tuning == null) {
+ return new BucketSplitting(cluster.isMemfilePersistence(), null, null, null);
+ }
+
+ ModelElement bucketSplitting = tuning.getChild("bucket-splitting");
+ if (bucketSplitting != null) {
+ Integer maxDocuments = bucketSplitting.getIntegerAttribute("max-documents");
+ Integer splitSize = bucketSplitting.getIntegerAttribute("max-size");
+ Integer minSplitCount = bucketSplitting.getIntegerAttribute("minimum-bits");
+
+ return new BucketSplitting(cluster.isMemfilePersistence(), maxDocuments, splitSize, minSplitCount);
+ }
+
+ return new BucketSplitting(cluster.isMemfilePersistence(), null, null, null);
+ }
+ }
+
+ public BucketSplitting(boolean useInlineBucketSplitting, Integer maxDocuments, Integer splitSize, Integer minSplitCount) {
+ this.maxDocuments = maxDocuments;
+ this.splitSize = splitSize;
+ this.minSplitCount = minSplitCount;
+ this.useInlineBucketSplitting = useInlineBucketSplitting;
+ }
+
+ @Override
+ public void getConfig(StorDistributormanagerConfig.Builder builder) {
+ if (maxDocuments != null) {
+ builder.splitcount(maxDocuments);
+ builder.joincount(maxDocuments / 2);
+ }
+ if (splitSize != null) {
+ builder.splitsize(splitSize);
+ builder.joinsize(splitSize / 2);
+ }
+ if (minSplitCount != null) {
+ builder.minsplitcount(minSplitCount);
+ }
+
+ builder.inlinebucketsplitting(useInlineBucketSplitting);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java
new file mode 100644
index 00000000000..fd1bc8a3362
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ClusterControllerConfig.java
@@ -0,0 +1,121 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.utils.Duration;
+import org.w3c.dom.Element;
+
+/**
+ * Config generation for common parameters for all fleet controllers.
+ *
+ * TODO: Author
+ */
+public class ClusterControllerConfig extends AbstractConfigProducer implements FleetcontrollerConfig.Producer {
+
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<ClusterControllerConfig> {
+ String clusterName;
+ ModelElement clusterElement;
+
+ public Builder(String clusterName, ModelElement clusterElement) {
+ this.clusterName = clusterName;
+ this.clusterElement = clusterElement;
+ }
+
+ @Override
+ protected ClusterControllerConfig doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ ModelElement tuning = null;
+
+ ModelElement clusterTuning = clusterElement.getChild("tuning");
+ if (clusterTuning != null) {
+ tuning = clusterTuning.getChild("cluster-controller");
+ }
+
+ if (tuning != null) {
+ return new ClusterControllerConfig(ancestor, clusterName,
+ tuning.childAsDuration("init-progress-time"),
+ tuning.childAsDuration("transition-time"),
+ tuning.childAsLong("max-premature-crashes"),
+ tuning.childAsDuration("stable-state-period"),
+ tuning.childAsDouble("min-distributor-up-ratio"),
+ tuning.childAsDouble("min-storage-up-ratio"),
+ clusterElement.childAsInteger("tuning.bucket-splitting.minimum-bits"));
+ } else {
+ return new ClusterControllerConfig(ancestor, clusterName, null, null, null, null, null, null,
+ clusterElement.childAsInteger("tuning.bucket-splitting.minimum-bits"));
+ }
+ }
+ }
+
+ String clusterName;
+ Duration initProgressTime;
+ Duration transitionTime;
+ Long maxPrematureCrashes;
+ Duration stableStateTimePeriod;
+ Double minDistributorUpRatio;
+ Double minStorageUpRatio;
+ Integer minSplitBits;
+
+ private ClusterControllerConfig(AbstractConfigProducer parent,
+ String clusterName,
+ Duration initProgressTime,
+ Duration transitionTime,
+ Long maxPrematureCrashes,
+ Duration stableStateTimePeriod,
+ Double minDistributorUpRatio,
+ Double minStorageUpRatio,
+ Integer minSplitBits) {
+ super(parent, "fleetcontroller");
+
+ this.clusterName = clusterName;
+ this.initProgressTime = initProgressTime;
+ this.transitionTime = transitionTime;
+ this.maxPrematureCrashes = maxPrematureCrashes;
+ this.stableStateTimePeriod = stableStateTimePeriod;
+ this.minDistributorUpRatio = minDistributorUpRatio;
+ this.minStorageUpRatio = minStorageUpRatio;
+ this.minSplitBits = minSplitBits;
+ }
+
+ @Override
+ public void getConfig(FleetcontrollerConfig.Builder builder) {
+ AbstractConfigProducerRoot root = getRoot();
+ if (root instanceof VespaModel) {
+ String zooKeeperAddress =
+ root.getAdmin().getZooKeepersConfigProvider().getZooKeepersConnectionSpec();
+ builder.zookeeper_server(zooKeeperAddress);
+ } else {
+ builder.zookeeper_server("");
+ }
+
+ builder.index(0);
+ builder.cluster_name(clusterName);
+ builder.fleet_controller_count(getChildren().size());
+
+ if (initProgressTime != null) {
+ builder.init_progress_time((int) initProgressTime.getMilliSeconds());
+ }
+ if (transitionTime != null) {
+ builder.storage_transition_time((int) transitionTime.getMilliSeconds());
+ }
+ if (maxPrematureCrashes != null) {
+ builder.max_premature_crashes(maxPrematureCrashes.intValue());
+ }
+ if (stableStateTimePeriod != null) {
+ builder.stable_state_time_period((int) stableStateTimePeriod.getMilliSeconds());
+ }
+ if (minDistributorUpRatio != null) {
+ builder.min_distributor_up_ratio(minDistributorUpRatio);
+ }
+ if (minStorageUpRatio != null) {
+ builder.min_storage_up_ratio(minStorageUpRatio);
+ }
+ if (minSplitBits != null) {
+ builder.ideal_distribution_bits(minSplitBits);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/Content.java b/config-model/src/main/java/com/yahoo/vespa/model/content/Content.java
new file mode 100644
index 00000000000..b9fe10e4873
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/Content.java
@@ -0,0 +1,347 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.ConfigModelRepoAdder;
+import com.yahoo.config.model.admin.AdminModel;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.model.*;
+import com.yahoo.vespa.model.builder.VespaModelBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.container.docproc.DocprocChains;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import com.yahoo.vespa.model.search.IndexingDocprocChain;
+import com.yahoo.vespa.model.search.SearchNode;
+
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * The config model from a content tag in services.
+ * This consists mostly of a ContentCluster.
+ *
+ * @author balder
+ */
+public class Content extends ConfigModel {
+
+ private static final Logger log = Logger.getLogger(Content.class.getName());
+
+ private ContentCluster cluster;
+ private Optional<ContainerCluster> ownedIndexingCluster = Optional.empty();
+ private final boolean hostedVespa;
+
+ // Dependencies to other models
+ private final AdminModel adminModel;
+ private final Collection<ContainerModel> containers; // to find or add the docproc container
+
+ @SuppressWarnings({ "UnusedDeclaration"}) // Created by reflection in ConfigModelRepo
+ public Content(ConfigModelContext modelContext, AdminModel adminModel, Collection<ContainerModel> containers) {
+ super(modelContext);
+ modelContext.getParentProducer().getRoot();
+ hostedVespa = modelContext.getDeployState().isHostedVespa();
+ this.adminModel = adminModel;
+ this.containers = containers;
+ }
+
+ /** Returns the admin model of this system */
+ public AdminModel adminModel() { return adminModel; }
+
+ /** Called by DomContentBuilder during build */
+ public void setCluster(ContentCluster cluster, ConfigModelContext configModelContext) {
+ this.cluster = cluster;
+ initializeIndexingClusters(containers,
+ configModelContext.getConfigModelRepoAdder(),
+ (ApplicationConfigProducerRoot)configModelContext.getParentProducer());
+ }
+
+ public ContentCluster getCluster() { return cluster; }
+
+ /**
+ * Returns indexing cluster implicitly created by this,
+ * or empty if an explicit cluster is used (or if called before the build phase)
+ */
+ public Optional<ContainerCluster> ownedIndexingCluster() { return ownedIndexingCluster; }
+
+ public void createTlds(ConfigModelRepo modelRepo) {
+ IndexedSearchCluster indexedCluster = cluster.getSearch().getIndexed();
+ if (indexedCluster == null) {
+ return;
+ }
+
+ SimpleConfigProducer tldParent = new SimpleConfigProducer(indexedCluster, "tlds");
+ for (ConfigModel model : modelRepo.asMap().values()) {
+ if (!(model instanceof ContainerModel)) {
+ continue;
+ }
+
+ ContainerCluster containerCluster = ((ContainerModel) model).getCluster();
+ if (containerCluster.getSearch() == null) {
+ continue; // this is not a qrs cluster
+ }
+
+ log.log(LogLevel.DEBUG, "Adding tlds for indexed cluster " + indexedCluster.getClusterName() + ", container cluster " + containerCluster.getName());
+ indexedCluster.addTldsWithSameIdsAsContainers(tldParent, containerCluster);
+ }
+ indexedCluster.setupDispatchGroups();
+ }
+
+ /** Select/creates and initializes the indexing cluster coupled to this */
+ private void initializeIndexingClusters(Collection<ContainerModel> containers,
+ ConfigModelRepoAdder configModelRepoAdder,
+ ApplicationConfigProducerRoot root) {
+ if (getCluster().getSearch().hasIndexedCluster())
+ initializeOrSetExistingIndexingCluster(getCluster().getSearch().getIndexed(), hostedVespa,
+ containers, configModelRepoAdder, root);
+ }
+
+ private void initializeOrSetExistingIndexingCluster(IndexedSearchCluster indexedSearchCluster,
+ boolean isHostedVespa,
+ Collection<ContainerModel> containers,
+ ConfigModelRepoAdder configModelRepoAdder,
+ ApplicationConfigProducerRoot root) {
+ if (indexedSearchCluster.hasExplicitIndexingCluster()) {
+ setExistingIndexingCluster(indexedSearchCluster, containers);
+ } else if (isHostedVespa) {
+ setContainerAsIndexingCluster(indexedSearchCluster, containers, configModelRepoAdder, root);
+ } else {
+ createImplicitIndexingCluster(indexedSearchCluster, configModelRepoAdder, root);
+ }
+ }
+
+ private void setContainerAsIndexingCluster(IndexedSearchCluster indexedSearchCluster,
+ Collection<ContainerModel> containers,
+ ConfigModelRepoAdder configModelRepoAdder,
+ ApplicationConfigProducerRoot root) {
+ if (containers.isEmpty()) {
+ createImplicitIndexingCluster(indexedSearchCluster, configModelRepoAdder, root);
+ } else {
+ ContainerCluster targetCluster = getContainerWithDocproc(containers);
+ if (targetCluster == null)
+ targetCluster = getContainerWithSearch(containers);
+ if (targetCluster == null)
+ targetCluster = containers.iterator().next().getCluster();
+
+ addDocproc(targetCluster);
+ indexedSearchCluster.setIndexingClusterName(targetCluster.getName());
+ addIndexingChainsTo(targetCluster, indexedSearchCluster);
+ }
+ }
+
+ private void setExistingIndexingCluster(IndexedSearchCluster cluster, Collection<ContainerModel> containers) {
+ String indexingClusterName = cluster.getIndexingClusterName();
+ ContainerModel containerModel = findByName(indexingClusterName, containers);
+ if (containerModel == null)
+ throw new RuntimeException("Content cluster '" + cluster.getClusterName() + "' refers to docproc " +
+ "cluster '" + indexingClusterName + "', but this cluster does not exist.");
+ addIndexingChainsTo(containerModel.getCluster(), cluster);
+ }
+
+ private ContainerModel findByName(String name, Collection<ContainerModel> containers) {
+ for (ContainerModel container : containers)
+ if (container.getId().equals(name))
+ return container;
+ return null;
+ }
+
+ private void addIndexingChainsTo(ContainerCluster indexer, IndexedSearchCluster cluster) {
+ addIndexingChain(indexer);
+ DocprocChain indexingChain;
+ ComponentRegistry<DocprocChain> allChains = indexer.getDocprocChains().allChains();
+ if (cluster.hasExplicitIndexingChain()) {
+ indexingChain = allChains.getComponent(cluster.getIndexingChainName());
+ if (indexingChain == null) {
+ throw new RuntimeException("Indexing cluster " + cluster.getClusterName() + " refers to docproc " +
+ "chain " + cluster.getIndexingChainName() + " for indexing, which does not exist.");
+ } else {
+ checkThatExplicitIndexingChainInheritsCorrectly(allChains, indexingChain.getChainSpecification());
+ }
+ } else {
+ indexingChain = allChains.getComponent(IndexingDocprocChain.NAME);
+ }
+
+ cluster.setIndexingChain(indexingChain);
+ }
+
+ private static boolean checkParentChain(ComponentRegistry<DocprocChain> allChains, ChainSpecification chainSpec) {
+ if (IndexingDocprocChain.NAME.equals(chainSpec.componentId.stringValue())) {
+ return true;
+ }
+
+ ChainSpecification.Inheritance inheritance = chainSpec.inheritance;
+ for (ComponentSpecification parentComponentSpec : inheritance.chainSpecifications) {
+ ChainSpecification parentSpec = getChainSpec(allChains, parentComponentSpec);
+ checkParentChain(allChains, parentSpec);
+ }
+
+ return false;
+ }
+
+ private static ChainSpecification getChainSpec(ComponentRegistry<DocprocChain> allChains, ComponentSpecification componentSpec) {
+ DocprocChain docprocChain = allChains.getComponent(componentSpec);
+ if (docprocChain == null) {
+ throw new IllegalArgumentException("Chain '" + componentSpec + "' not found.");
+ }
+ return docprocChain.getChainSpecification();
+ }
+
+ private static void addIndexingChain(ContainerCluster containerCluster) {
+ DocprocChain chainAlreadyPresent = containerCluster.getDocprocChains().allChains().
+ getComponent(new ComponentId(IndexingDocprocChain.NAME));
+ if (chainAlreadyPresent != null) {
+ if (chainAlreadyPresent instanceof IndexingDocprocChain) {
+ return;
+ } else {
+ throw new IllegalArgumentException("A docproc chain may not have the ID '" +
+ IndexingDocprocChain.NAME + ", since this is reserved by Vespa. Please use a different ID.");
+ }
+ }
+
+ containerCluster.getDocprocChains().add(new IndexingDocprocChain());
+ }
+
+ /** Create a new container cluster for indexing and add it to the Vespa model */
+ private void createImplicitIndexingCluster(IndexedSearchCluster cluster,
+ ConfigModelRepoAdder configModelRepoAdder,
+ ApplicationConfigProducerRoot root) {
+ String indexerName = cluster.getIndexingClusterName();
+ AbstractConfigProducer p = root.getChildren().get(ContainerModel.DOCPROC_RESERVED_NAME);
+ if (p == null)
+ p = new SimpleConfigProducer(root, ContainerModel.DOCPROC_RESERVED_NAME);
+ ConfigModelContext context = ConfigModelContext.createFromParentAndId(configModelRepoAdder, p, ContainerModel.DOCPROC_RESERVED_NAME);
+ ContainerCluster indexingCluster = new ContainerCluster(context.getParentProducer(), "cluster." + indexerName, indexerName);
+ ContainerModel indexingClusterModel = new ContainerModel(ConfigModelContext.createFromParentAndId(configModelRepoAdder, p, indexingCluster.getSubId()));
+ indexingClusterModel.setCluster(indexingCluster);
+ configModelRepoAdder.add(indexingClusterModel);
+ ownedIndexingCluster = Optional.of(indexingCluster);
+
+ ContainerModelBuilder.addDefaultHandler_legacyBuilder(indexingCluster);
+
+ addDocproc(indexingCluster);
+
+ List<Container> nodes = new ArrayList<>();
+ int index = 0;
+ Set<HostResource> processedHosts = new LinkedHashSet<>();
+ boolean isElastic = cluster.isElastic();
+ for (SearchNode searchNode : cluster.getSearchNodes()) {
+ HostResource host = searchNode.getHostResource();
+ if (!processedHosts.contains(host)) {
+ String containerName = String.valueOf(isElastic ? searchNode.getDistributionKey() : index++);
+ Container docprocService = new Container(indexingCluster, containerName);
+ docprocService.setBasePort(host.nextAvailableBaseport(docprocService.getPortCount()));
+ docprocService.setHostResource(host);
+ docprocService.initService();
+ nodes.add(docprocService);
+ processedHosts.add(host);
+ }
+ }
+ indexingCluster.addContainers(nodes);
+
+ addIndexingChain(indexingCluster);
+ cluster.setIndexingChain(indexingCluster.getDocprocChains().allChains().getComponent(IndexingDocprocChain.NAME));
+ }
+
+ private void addDocproc(ContainerCluster cluster) {
+ if (cluster.getDocproc() == null) {
+ DocprocChains chains = new DocprocChains(cluster, "docprocchains");
+ ContainerDocproc containerDocproc = new ContainerDocproc(cluster, chains);
+ cluster.setDocproc(containerDocproc);
+ }
+ }
+
+ private ContainerCluster getContainerWithDocproc(Collection<ContainerModel> containers) {
+ for (ContainerModel container : containers)
+ if (container.getCluster().getDocproc() != null)
+ return container.getCluster();
+ return null;
+ }
+
+ private static ContainerCluster getContainerWithSearch(Collection<ContainerModel> containers) {
+ for (ContainerModel container : containers)
+ if (container.getCluster().getSearch() != null)
+ return container.getCluster();
+ return null;
+ }
+
+ private static void checkThatExplicitIndexingChainInheritsCorrectly(ComponentRegistry<DocprocChain> allChains, ChainSpecification chainSpec) {
+ ChainSpecification.Inheritance inheritance = chainSpec.inheritance;
+ boolean found = false;
+ for (ComponentSpecification componentSpec : inheritance.chainSpecifications) {
+ ChainSpecification parentSpec = getChainSpec(allChains, componentSpec);
+ found = checkParentChain(allChains, parentSpec);
+ if (found) {
+ break;
+ }
+ }
+ if (!found) {
+ throw new IllegalArgumentException("Docproc chain '" + chainSpec.componentId + "' does not inherit from 'indexing' chain.");
+ }
+ }
+
+ public static List<Content> getContent(ConfigModelRepo pc) {
+ List<Content> contents = new ArrayList<>();
+
+ for (ConfigModel model : pc.asMap().values()) {
+ if (model instanceof Content) {
+ contents.add((Content)model);
+ }
+ }
+
+ return contents;
+ }
+
+ public static List<AbstractSearchCluster> getSearchClusters(ConfigModelRepo pc) {
+ List<AbstractSearchCluster> clusters = new ArrayList<>();
+
+ for (ContentCluster c : getContentClusters(pc)) {
+ clusters.addAll(c.getSearch().getClusters().values());
+ }
+
+ return clusters;
+ }
+
+ public static List<ContentCluster> getContentClusters(ConfigModelRepo pc) {
+ List<ContentCluster> clusters = new ArrayList<>();
+
+ for (Content c : getContent(pc)) {
+ clusters.add(c.getCluster());
+ }
+
+ return clusters;
+ }
+
+ @Override
+ public void prepare(ConfigModelRepo models) {
+ if (cluster.getRootGroup().useCpuSocketAffinity()) {
+ setCpuSocketAffinity();
+ }
+ if (cluster.getRootGroup().getMmapNoCoreLimit().isPresent()) {
+ for (AbstractService s : cluster.getSearch().getSearchNodes()) {
+ s.setMMapNoCoreLimit(cluster.getRootGroup().getMmapNoCoreLimit().get());
+ }
+ }
+ cluster.prepare();
+ }
+
+ private void setCpuSocketAffinity() {
+ // Currently only distribute affinity for search nodes
+ AbstractService.distributeCpuSocketAffinity(cluster.getSearch().getSearchNodes());
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ContentNode.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentNode.java
new file mode 100644
index 00000000000..77a90e8a515
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentNode.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.metrics.MetricsmanagerConfig;
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.vespa.config.content.core.StorCommunicationmanagerConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.config.content.core.StorStatusConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+
+/**
+ * Common class for config producers for storage and distributor nodes.
+ *
+ * TODO: Author
+ */
+@RestartConfigs({StorCommunicationmanagerConfig.class, StorStatusConfig.class,
+ StorServerConfig.class, LoadTypeConfig.class, MetricsmanagerConfig.class})
+public abstract class ContentNode extends AbstractService
+ implements StorCommunicationmanagerConfig.Producer, StorStatusConfig.Producer, StorServerConfig.Producer {
+
+ protected int distributionKey;
+ String rootDirectory;
+
+ public ContentNode(AbstractConfigProducer parent, String clusterName, String rootDirectory, int distributionKey) {
+ super(parent, "" + distributionKey);
+ this.distributionKey = distributionKey;
+ initialize(distributionKey);
+
+ this.rootDirectory = rootDirectory;
+
+ setProp("clustertype", "content");
+ setProp("clustername", clusterName);
+ setProp("index", distributionKey);
+ }
+
+ public int getDistributionKey() {
+ return distributionKey;
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ builder.root_folder(rootDirectory);
+ builder.node_index(distributionKey);
+ }
+
+ public void initialize(int distributionKey) {
+ this.distributionKey = distributionKey;
+ portsMeta.on(0).tag("messaging");
+ portsMeta.on(1).tag("rpc").tag("status");
+ portsMeta.on(2).tag("http").tag("status").tag("state");
+
+ monitorService();
+ }
+
+ @Override
+ public int getPortCount() { return 3; }
+
+
+ @Override
+ public void getConfig(StorCommunicationmanagerConfig.Builder builder) {
+ builder.mbusport(getRelativePort(0));
+ builder.rpcport(getRelativePort(1));
+ }
+
+ @Override
+ public void getConfig(StorStatusConfig.Builder builder) {
+ builder.httpport(getRelativePort(2));
+ }
+
+ @Override
+ public int getHealthPort() {
+ return getRelativePort(2);
+ }
+
+ public String getRootDirectory() {
+ return rootDirectory;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearch.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearch.java
new file mode 100644
index 00000000000..fdfb7f75188
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearch.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ContentSearch {
+
+ private final Double queryTimeout;
+ private final Double visibilityDelay;
+
+ private ContentSearch(Builder builder) {
+ queryTimeout = builder.queryTimeout;
+ visibilityDelay = builder.visibilityDelay;
+ }
+
+ public Double getQueryTimeout() {
+ return queryTimeout;
+ }
+
+ public Double getVisibilityDelay() {
+ return visibilityDelay;
+ }
+
+ public static class Builder {
+
+ private Double queryTimeout;
+ private Double visibilityDelay;
+
+ public ContentSearch build() {
+ return new ContentSearch(this);
+ }
+
+ public Builder setQueryTimeout(Double queryTimeout) {
+ this.queryTimeout = queryTimeout;
+ return this;
+ }
+
+ public Builder setVisibilityDelay(Double visibilityDelay) {
+ this.visibilityDelay = visibilityDelay;
+ return this;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java
new file mode 100644
index 00000000000..b5cbb23233a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ContentSearchCluster.java
@@ -0,0 +1,299 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.search.DispatchConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.documentmodel.DocumentTypeRepo;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.builder.UserConfigBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.DomSearchTuningBuilder;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.cluster.DomResourceLimitsBuilder;
+import com.yahoo.vespa.model.search.*;
+import org.w3c.dom.Element;
+
+
+import java.util.*;
+
+/**
+ * Encapsulates the various options for search in a content model.
+ * Wraps a search cluster from com.yahoo.vespa.model.search.
+ */
+public class ContentSearchCluster extends AbstractConfigProducer implements ProtonConfig.Producer, DispatchConfig.Producer {
+
+ private final boolean flushOnShutdown;
+
+ /** If this is set up for streaming search, it is modelled as one search cluster per search definition */
+ private Map<String, AbstractSearchCluster> clusters = new TreeMap<>();
+
+ /** The single, indexed search cluster this sets up (supporting multiple document types), or null if none */
+ private IndexedSearchCluster indexedCluster;
+
+ private final String clusterName;
+ Map<String, NewDocumentType> documentDefinitions;
+
+ /** The search nodes of this if it does not have an indexed cluster */
+ List<SearchNode> nonIndexed = new ArrayList<>();
+
+ Map<StorageGroup, NodeSpec> groupToSpecMap = new LinkedHashMap<>();
+ private DocumentTypeRepo repo = null;
+ private Optional<ResourceLimits> resourceLimits = Optional.empty();
+
+ public void prepare() {
+ repo = getRoot().getDeployState().getDocumentModel().getDocumentManager();
+ }
+
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<ContentSearchCluster> {
+
+ private final Map<String, NewDocumentType> documentDefinitions;
+
+ public Builder(Map<String, NewDocumentType> documentDefinitions) {
+ this.documentDefinitions = documentDefinitions;
+ }
+
+ @Override
+ protected ContentSearchCluster doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ ModelElement clusterElem = new ModelElement(producerSpec);
+ String clusterName = ContentCluster.getClusterName(clusterElem);
+ Boolean flushOnShutdown = clusterElem.childAsBoolean("engine.proton.flush-on-shutdown");
+
+ ContentSearchCluster search = new ContentSearchCluster(ancestor, clusterName, documentDefinitions, flushOnShutdown != null ? flushOnShutdown : false);
+
+ ModelElement tuning = clusterElem.getChildByPath("engine.proton.tuning");
+ if (tuning != null) {
+ search.setTuning(new DomSearchTuningBuilder().build(search, tuning.getXml()));
+ }
+ ModelElement protonElem = clusterElem.getChildByPath("engine.proton");
+ if (protonElem != null) {
+ search.setResourceLimits(DomResourceLimitsBuilder.build(protonElem));
+ }
+
+ buildAllStreamingSearchClusters(clusterElem, clusterName, search);
+ buildIndexedSearchCluster(clusterElem, clusterName, search);
+ return search;
+ }
+
+ private Double getQueryTimeout(ModelElement clusterElem) {
+ return clusterElem.childAsDouble("engine.proton.query-timeout");
+ }
+
+ private void buildAllStreamingSearchClusters(ModelElement clusterElem, String clusterName, ContentSearchCluster search) {
+ ModelElement docElem = clusterElem.getChild("documents");
+
+ if (docElem == null) {
+ return;
+ }
+
+ for (ModelElement docType : docElem.subElements("document")) {
+ String mode = docType.getStringAttribute("mode");
+ if ("streaming".equals(mode)) {
+ buildStreamingSearchCluster(clusterElem, clusterName, search, docType);
+ }
+ }
+ }
+
+ private void buildStreamingSearchCluster(ModelElement clusterElem, String clusterName, ContentSearchCluster search, ModelElement docType) {
+ StreamingSearchCluster cluster = new StreamingSearchCluster(search, clusterName + "." + docType.getStringAttribute("type"), 0, clusterName, clusterName);
+
+ List<ModelElement> def = new ArrayList<>();
+ def.add(docType);
+ search.addSearchCluster(cluster, getQueryTimeout(clusterElem), def);
+ }
+
+ private void buildIndexedSearchCluster(ModelElement clusterElem,
+ String clusterName,
+ ContentSearchCluster search) {
+ List<ModelElement> indexedDefs = getIndexedSearchDefinitions(clusterElem);
+ if (!indexedDefs.isEmpty()) {
+ IndexedSearchCluster isc = new IndexedElasticSearchCluster(search, clusterName, 0);
+ isc.setRoutingSelector(clusterElem.childAsString("documents.selection"));
+
+ Double visibilityDelay = clusterElem.childAsDouble("engine.proton.visibility-delay");
+ if (visibilityDelay != null) {
+ isc.setVisibilityDelay(visibilityDelay);
+ }
+
+ search.addSearchCluster(isc, getQueryTimeout(clusterElem), indexedDefs);
+ }
+ }
+
+ private List<ModelElement> getIndexedSearchDefinitions(ModelElement clusterElem) {
+ List<ModelElement> indexedDefs = new ArrayList<>();
+ ModelElement docElem = clusterElem.getChild("documents");
+ if (docElem == null) {
+ return indexedDefs;
+ }
+
+ for (ModelElement docType : docElem.subElements("document")) {
+ String mode = docType.getStringAttribute("mode");
+ if ("index".equals(mode)) {
+ indexedDefs.add(docType);
+ }
+ }
+ return indexedDefs;
+ }
+ }
+
+ private ContentSearchCluster(AbstractConfigProducer parent,
+ String clusterName,
+ Map<String, NewDocumentType> documentDefinitions, boolean flushOnShutdown)
+ {
+ super(parent, "search");
+ this.clusterName = clusterName;
+ this.documentDefinitions = documentDefinitions;
+ this.flushOnShutdown = flushOnShutdown;
+ }
+
+ void addSearchCluster(SearchCluster cluster, Double queryTimeout, List<ModelElement> documentDefs) {
+ addSearchDefinitions(documentDefs, cluster);
+
+ if (queryTimeout != null) {
+ cluster.setQueryTimeout(queryTimeout);
+ }
+ cluster.defaultDocumentsConfig();
+ cluster.deriveSearchDefinitions(new ArrayList<>());
+ addCluster(cluster);
+ }
+
+ private void addSearchDefinitions(List<ModelElement> searchDefs, AbstractSearchCluster sc) {
+ for (ModelElement e : searchDefs) {
+ SearchDefinitionXMLHandler searchDefinitionXMLHandler = new SearchDefinitionXMLHandler(e);
+ SearchDefinition searchDefinition =
+ searchDefinitionXMLHandler.getResponsibleSearchDefinition(sc.getRoot().getDeployState().getSearchDefinitions());
+ if (searchDefinition == null)
+ throw new RuntimeException("Search definition parsing error or file does not exist: '" +
+ searchDefinitionXMLHandler.getName() + "'");
+
+ // TODO: remove explicit building of user configs when the complete content model is built using builders.
+ sc.getLocalSDS().add(new AbstractSearchCluster.SearchDefinitionSpec(searchDefinition,
+ UserConfigBuilder.build(e.getXml(), sc.getRoot().getDeployState(), sc.getRoot().deployLogger())));
+ //need to get the document names from this sdfile
+ sc.addDocumentNames(searchDefinition);
+ }
+ }
+
+ public ContentSearchCluster addCluster(AbstractSearchCluster sc) {
+ if (clusters.containsKey(sc.getClusterName())) {
+ throw new IllegalArgumentException("I already have registered cluster '" + sc.getClusterName() + "'");
+ }
+ if (sc instanceof IndexedSearchCluster) {
+ if (indexedCluster != null) {
+ throw new IllegalArgumentException("I already have one indexed cluster named '" + indexedCluster.getClusterName());
+ }
+ indexedCluster = (IndexedSearchCluster)sc;
+ }
+ clusters.put(sc.getClusterName(), sc);
+ return this;
+ }
+
+ public List<SearchNode> getSearchNodes() {
+ return hasIndexedCluster() ? getIndexed().getSearchNodes() : nonIndexed;
+ }
+
+ public SearchNode addSearchNode(ContentNode node, StorageGroup parentGroup, ModelElement element) {
+ AbstractConfigProducer parent = hasIndexedCluster() ? getIndexed() : this;
+
+ NodeSpec spec = getNextSearchNodeSpec(parentGroup);
+ SearchNode snode;
+ TransactionLogServer tls;
+ if (element == null) {
+ snode = SearchNode.create(parent, "" + node.getDistributionKey(), node.getDistributionKey(), spec, clusterName, node, flushOnShutdown);
+ snode.setHostResource(node.getHostResource());
+ snode.initService();
+
+ tls = new TransactionLogServer(snode, clusterName);
+ tls.setHostResource(snode.getHostResource());
+ tls.initService();
+ } else {
+ snode = new SearchNode.Builder(""+node.getDistributionKey(), spec, clusterName, node, flushOnShutdown).build(parent, element.getXml());
+ tls = new TransactionLogServer.Builder(clusterName).build(snode, element.getXml());
+ }
+ snode.setTls(tls);
+ if (hasIndexedCluster()) {
+ getIndexed().addSearcher(snode);
+ } else {
+ nonIndexed.add(snode);
+ }
+ return snode;
+ }
+
+ private NodeSpec getNextSearchNodeSpec(StorageGroup parentGroup) {
+ NodeSpec spec = groupToSpecMap.get(parentGroup);
+ if (spec == null) {
+ spec = new NodeSpec(groupToSpecMap.size(), 0);
+ } else {
+ spec = new NodeSpec(spec.rowId(), spec.partitionId() + 1);
+ }
+ groupToSpecMap.put(parentGroup, spec);
+ return spec;
+ }
+
+ private Tuning tuning;
+
+ public void setTuning(Tuning t) {
+ tuning = t;
+ }
+
+ public void setResourceLimits(ResourceLimits resourceLimits) {
+ this.resourceLimits = Optional.of(resourceLimits);
+ }
+
+ public boolean usesHierarchicDistribution() {
+ return indexedCluster != null && groupToSpecMap.size() > 1;
+ }
+
+ public void handleRedundancy(Redundancy redundancy) {
+ if (usesHierarchicDistribution()) {
+ indexedCluster.setMaxNodesDownPerFixedRow((redundancy.effectiveFinalRedundancy() / groupToSpecMap.size()) - 1);
+ }
+ }
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ double visibilityDelay = hasIndexedCluster() ? getIndexed().getVisibilityDelay() : 0.0;
+ for (NewDocumentType type : documentDefinitions.values()) {
+ ProtonConfig.Documentdb.Builder ddbB = new ProtonConfig.Documentdb.Builder();
+ ddbB.inputdoctypename(type.getFullName().getName())
+ .configid(getConfigId())
+ .visibilitydelay(visibilityDelay);
+ if (hasIndexedCluster()) {
+ getIndexed().fillDocumentDBConfig(type.getFullName().getName(), ddbB);
+ }
+ builder.documentdb(ddbB);
+ }
+ for (AbstractSearchCluster sc : getClusters().values()) {
+ if (sc instanceof StreamingSearchCluster) {
+ NewDocumentType type = repo.getDocumentType(((StreamingSearchCluster)sc).getSdConfig().getSearch().getName());
+ ProtonConfig.Documentdb.Builder ddbB = new ProtonConfig.Documentdb.Builder();
+ ddbB.inputdoctypename(type.getFullName().getName()).configid(getConfigId());
+ builder.documentdb(ddbB);
+ }
+ }
+ int numDocumentDbs = builder.documentdb.size();
+ builder.initialize(new ProtonConfig.Initialize.Builder().threads(numDocumentDbs + 1));
+
+ if (resourceLimits.isPresent()) {
+ resourceLimits.get().getConfig(builder);
+ }
+
+ if (tuning != null) {
+ tuning.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(DispatchConfig.Builder builder) {
+ if (hasIndexedCluster()) {
+ getIndexed().getConfig(builder);
+ }
+ }
+
+ public Map<String, AbstractSearchCluster> getClusters() { return clusters; }
+ public IndexedSearchCluster getIndexed() { return indexedCluster; }
+ public boolean hasIndexedCluster() { return indexedCluster != null; }
+ public String getClusterName() { return clusterName; }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/DispatchSpec.java b/config-model/src/main/java/com/yahoo/vespa/model/content/DispatchSpec.java
new file mode 100644
index 00000000000..ad69cced45b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/DispatchSpec.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents the dispatch setup for a content cluster.
+ * This EITHER has a number of dispatch groups OR a an explicit list of groups,
+ * for unknown reasons (talk to Geir).
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class DispatchSpec {
+
+ private final Integer numDispatchGroups;
+ private final List<Group> groups;
+
+ private DispatchSpec(Builder builder) {
+ numDispatchGroups = builder.numDispatchGroups;
+ groups = builder.groups;
+ }
+
+ public Integer getNumDispatchGroups() { return numDispatchGroups; }
+
+ public List<Group> getGroups() {
+ return groups;
+ }
+
+ public boolean valid() {
+ return numDispatchGroups != null || groups != null;
+ }
+
+ /**
+ * Reference to a node which is contained in a dispatch group.
+ */
+ public static class Node {
+ private final int distributionKey;
+ public Node(int distributionKey) {
+ this.distributionKey = distributionKey;
+ }
+ public int getDistributionKey() {
+ return distributionKey;
+ }
+ }
+
+ /**
+ * A dispatch group with a list of nodes contained in that group.
+ */
+ public static class Group {
+ private final List<Node> nodes = new ArrayList<>();
+ public Group() {
+
+ }
+ public Group addNode(Node node) {
+ nodes.add(node);
+ return this;
+ }
+ public List<Node> getNodes() {
+ return nodes;
+ }
+ }
+
+ public static class Builder {
+
+ private Integer numDispatchGroups;
+ private List<Group> groups;
+
+ public DispatchSpec build() {
+ return new DispatchSpec(this);
+ }
+
+ public Builder setNumDispatchGroups(Integer numDispatchGroups) {
+ this.numDispatchGroups = numDispatchGroups;
+ return this;
+ }
+
+ public Builder setGroups(List<Group> groups) {
+ this.groups = groups;
+ return this;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/DistributionBitCalculator.java b/config-model/src/main/java/com/yahoo/vespa/model/content/DistributionBitCalculator.java
new file mode 100644
index 00000000000..b6462d5aa9b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/DistributionBitCalculator.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+public class DistributionBitCalculator {
+
+ public static int getDistributionBits(int nodes, ContentCluster.DistributionMode mode) {
+ if (mode == ContentCluster.DistributionMode.STRICT) {
+ if (nodes < 5) {
+ return 8; // Require few buckets in small clusters to ease testing
+ } else if (nodes < 15) {
+ return 16;
+ } else if (nodes < 200) {
+ return 21;
+ } else if (nodes < 800) {
+ return 25;
+ } else if (nodes < 1500) {
+ return 28;
+ } else if (nodes < 5000) {
+ return 30;
+ } else {
+ return 32;
+ }
+ } else if (mode == ContentCluster.DistributionMode.LOOSE) {
+ if (nodes < 5) {
+ return 8;
+ } else if (nodes < 200) {
+ return 16;
+ } else {
+ return 24;
+ }
+ } else if (mode == ContentCluster.DistributionMode.LEGACY) {
+ if (nodes == 1) { // min 1 bucket/node
+ return 1;
+ } else if (nodes == 2) { // min 128 buckets/node
+ return 8;
+ } else if (nodes <= 6) { // min 5462 buckets/node
+ return 14;
+ } else if (nodes <= 8) { // min 9362 buckets/node
+ return 16;
+ } else if (nodes <= 10) { // min 14563 buckets/node
+ return 17;
+ } else if (nodes <= 12) { // min 23832 buckets/node
+ return 18;
+ } else if (nodes <= 20) { // min 40329 buckets/node
+ return 19;
+ } else if (nodes <= 32) { // min 49933 buckets/node
+ return 20;
+ } else if (nodes <= 64) { // min 63550 buckets/node
+ return 21;
+ } else if (nodes <= 100) { // min 64528 buckets/node
+ return 22;
+ } else if (nodes <= 256) { // min 83056 buckets/node
+ return 23;
+ } else if (nodes <= 350) { // min 65281 buckets/node
+ return 24;
+ } else if (nodes <= 500) { // min 95597 buckets/node
+ return 25;
+ } else if (nodes <= 1024) { // min 133950 buckets/node
+ return 26;
+ } else if (nodes <= 2048) { // min 130944 buckets/node
+ return 27;
+ } else if (nodes <= 4096) { // min 130008 buckets/node
+ return 28;
+ } else if (nodes <= 8192) { // min 131040 buckets/node
+ return 29;
+ } else if (nodes <= 16384) { // min 131056 buckets/node
+ return 30;
+ } else if (nodes <= 32768) { // min 131064 buckets/node
+ return 31;
+ } else { // min 131068 buckets/node
+ return 32;
+ }
+ } else {
+ throw new IllegalArgumentException("We don't know how to handle mode " + mode);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/Distributor.java b/config-model/src/main/java/com/yahoo/vespa/model/content/Distributor.java
new file mode 100644
index 00000000000..5458bd366d5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/Distributor.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.content.engines.PersistenceEngine;
+import org.w3c.dom.Element;
+
+/**
+ * Represents specific configuration for a given distributor node.
+ */
+public class Distributor extends ContentNode {
+
+ PersistenceEngine provider;
+
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<Distributor> {
+ ModelElement clusterXml;
+ PersistenceEngine persistenceProvider;
+
+ public Builder(ModelElement clusterXml, PersistenceEngine persistenceProvider) {
+ this.clusterXml = clusterXml;
+ this.persistenceProvider = persistenceProvider;
+ }
+
+ @Override
+ protected Distributor doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ return new Distributor((DistributorCluster)ancestor, new ModelElement(producerSpec).getIntegerAttribute("distribution-key"),
+ clusterXml.getIntegerAttribute("distributor-base-port"), persistenceProvider);
+ }
+ }
+
+ Distributor(DistributorCluster parent, int distributionKey, Integer distributorBasePort, PersistenceEngine provider) {
+ super(parent, parent.getClusterName(), StorageNode.rootFolder + parent.getClusterName() + "/distributor/"
+ + distributionKey, distributionKey);
+
+ this.provider = provider;
+
+ if (distributorBasePort != null) {
+ setBasePort(distributorBasePort);
+ }
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ super.getConfig(builder);
+ provider.getConfig(builder);
+ }
+
+ @Override
+ public String getStartupCommand() {
+ return "exec sbin/distributord -c $VESPA_CONFIG_ID";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java
new file mode 100644
index 00000000000..dc27da7461d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java
@@ -0,0 +1,151 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.core.StorDistributormanagerConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.document.select.DocumentSelector;
+import com.yahoo.document.select.parser.ParseException;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.metrics.MetricsmanagerConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import org.w3c.dom.Element;
+
+import java.util.logging.Logger;
+
+
+/**
+ * Generates distributor-specific configuration.
+ */
+public class DistributorCluster extends AbstractConfigProducer<Distributor>
+ implements StorDistributormanagerConfig.Producer, StorServerConfig.Producer, MetricsmanagerConfig.Producer {
+
+ public static final Logger log = Logger.getLogger(DistributorCluster.class.getPackage().toString());
+
+ private static class GcOptions {
+ public final int interval;
+ public final String selection;
+
+ public GcOptions(int interval, String selection) {
+ this.interval = interval;
+ this.selection = selection;
+ }
+ }
+
+ private final ContentCluster parent;
+ private final BucketSplitting bucketSplitting;
+ private final GcOptions gc;
+ private final boolean hasIndexedDocumentType;
+
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<DistributorCluster> {
+ ContentCluster parent;
+
+ public Builder(ContentCluster parent) {
+ this.parent = parent;
+ }
+
+ private String prepareGCSelection(ModelElement documentNode, String selStr) throws ParseException {
+ DocumentSelector s = new DocumentSelector(selStr);
+ boolean enableGC = false;
+ if (documentNode != null) {
+ enableGC = documentNode.getBooleanAttribute("garbage-collection", false);
+ }
+ if (!enableGC) {
+ return null;
+ }
+
+ return s.toString();
+ }
+
+ private int getGCInterval(ModelElement documentNode) throws ParseException {
+ int gcInterval = 3600;
+ if (documentNode != null) {
+ gcInterval = documentNode.getIntegerAttribute("garbage-collection-interval", gcInterval);
+ }
+ return gcInterval;
+ }
+
+ private GcOptions parseGcOptions(ModelElement documentNode) {
+ String gcSelection = parent.getRoutingSelector();
+ int gcInterval;
+ try {
+ if (gcSelection != null) {
+ gcSelection = prepareGCSelection(documentNode, gcSelection);
+ }
+ gcInterval = getGCInterval(documentNode);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Failed to parse garbage collection selection", e);
+ }
+ return new GcOptions(gcInterval, gcSelection);
+ }
+
+ private boolean documentModeImpliesIndexing(String mode) {
+ return "index".equals(mode);
+ }
+
+ private boolean clusterContainsIndexedDocumentType(ModelElement documentsNode) {
+ // The presence of at least one <document> and its mode attribute is schema-enforced.
+ return documentsNode.subElements("document").stream()
+ .anyMatch(node -> documentModeImpliesIndexing(node.getStringAttribute("mode")));
+ }
+
+ @Override
+ protected DistributorCluster doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ final ModelElement clusterElement = new ModelElement(producerSpec);
+ final ModelElement documentsNode = clusterElement.getChild("documents");
+ final GcOptions gc = parseGcOptions(documentsNode);
+ final boolean hasIndexedDocumentType = clusterContainsIndexedDocumentType(documentsNode);
+
+ return new DistributorCluster(parent,
+ new BucketSplitting.Builder().build(
+ parent, new ModelElement(producerSpec)), gc, hasIndexedDocumentType);
+ }
+ }
+
+ private DistributorCluster(ContentCluster parent, BucketSplitting bucketSplitting,
+ GcOptions gc, boolean hasIndexedDocumentType)
+ {
+ super(parent, "distributor");
+ this.parent = parent;
+ this.bucketSplitting = bucketSplitting;
+ this.gc = gc;
+ this.hasIndexedDocumentType = hasIndexedDocumentType;
+ }
+
+ @Override
+ public void getConfig(StorDistributormanagerConfig.Builder builder) {
+ if (gc.selection != null) {
+ builder.garbagecollection(new StorDistributormanagerConfig.Garbagecollection.Builder()
+ .selectiontoremove("not (" + gc.selection + ")")
+ .interval(gc.interval));
+ }
+ builder.enable_revert(parent.getPersistence().supportRevert());
+ builder.disable_bucket_activation(hasIndexedDocumentType == false);
+
+ bucketSplitting.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(MetricsmanagerConfig.Builder builder) {
+ ContentCluster.getMetricBuilder("log", builder).
+ addedmetrics("vds.distributor.docsstored").
+ addedmetrics("vds.distributor.bytesstored").
+ addedmetrics("vds.idealstate.delete_bucket.done_ok").
+ addedmetrics("vds.idealstate.merge_bucket.done_ok").
+ addedmetrics("vds.idealstate.split_bucket.done_ok").
+ addedmetrics("vds.idealstate.join_bucket.done_ok").
+ addedmetrics("vds.idealstate.buckets_rechecking");
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ builder.root_folder("");
+ builder.cluster_name(parent.getName());
+ builder.is_distributor(true);
+ }
+
+ public String getClusterName() {
+ return parent.getName();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/DocumentTypeVisitor.java b/config-model/src/main/java/com/yahoo/vespa/model/content/DocumentTypeVisitor.java
new file mode 100644
index 00000000000..585ae691773
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/DocumentTypeVisitor.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.document.select.Visitor;
+import com.yahoo.document.select.rule.*;
+
+public abstract class DocumentTypeVisitor implements Visitor {
+ @Override
+ public void visit(ArithmeticNode arithmeticNode) {
+ for (ArithmeticNode.NodeItem item : arithmeticNode.getItems()) {
+ item.getNode().accept(this);
+ }
+ }
+
+ @Override
+ public void visit(AttributeNode attributeNode) {
+ attributeNode.getValue().accept(this);
+ }
+
+ @Override
+ public void visit(ComparisonNode comparisonNode) {
+ comparisonNode.getLHS().accept(this);
+ comparisonNode.getRHS().accept(this);
+ }
+
+ @Override
+ public void visit(EmbracedNode embracedNode) {
+ embracedNode.getNode().accept(this);
+ }
+
+ @Override
+ public void visit(IdNode idNode) {
+ }
+
+ @Override
+ public void visit(LiteralNode literalNode) {
+ }
+
+ @Override
+ public void visit(LogicNode logicNode) {
+ for (LogicNode.NodeItem item : logicNode.getItems()) {
+ item.getNode().accept(this);
+ }
+ }
+
+ @Override
+ public void visit(NegationNode negationNode) {
+ negationNode.getNode().accept(this);
+ }
+
+ @Override
+ public void visit(NowNode nowNode) {
+ }
+
+ @Override
+ public void visit(SearchColumnNode searchColumnNode) {
+ }
+
+ @Override
+ public void visit(VariableNode variableNode) {
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java
new file mode 100644
index 00000000000..861088a80f5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionValidator.java
@@ -0,0 +1,117 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Class used to validate that hierarchic distribution is correctly setup when having an indexed content cluster.
+ *
+ * Note that this class does not implement the com.yahoo.vespa.model.application.validation.Validator interface,
+ * but is instead used in the context of com.yahoo.vespa.model.ConfigProducer.validate() such that it can be unit tested
+ * without having to build the complete vespa model.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class IndexedHierarchicDistributionValidator {
+
+ private final String clusterName;
+ private final StorageGroup rootGroup;
+ private final Redundancy redundancy;
+ private final TuningDispatch.DispatchPolicy dispatchPolicy;
+
+ public IndexedHierarchicDistributionValidator(String clusterName,
+ StorageGroup rootGroup,
+ Redundancy redundancy,
+ TuningDispatch.DispatchPolicy dispatchPolicy) {
+ this.clusterName = clusterName;
+ this.rootGroup = rootGroup;
+ this.redundancy = redundancy;
+ this.dispatchPolicy = dispatchPolicy;
+ }
+
+ public void validate() throws Exception {
+ validateThatWeHaveOneGroupLevel();
+ validateThatLeafGroupsHasEqualNumberOfNodes();
+ validateThatLeafGroupsCountIsAFactorOfRedundancy();
+ validateThatRedundancyPerGroupIsEqual();
+ validateThatReadyCopiesIsCompatibleWithRedundancy(rootGroup.getSubgroups().size());
+ }
+
+ private void validateThatWeHaveOneGroupLevel() {
+ for (StorageGroup group : rootGroup.getSubgroups()) {
+ if (group.getSubgroups().size() > 0) {
+ throw new IllegalArgumentException(getErrorMsgPrefix() + "Expected all groups under root group '" +
+ rootGroup.getName() + "' to be leaf groups only containing nodes, but sub group '" + group.getName() + "' contains " +
+ group.getSubgroups().size() + " sub groups.");
+ }
+ }
+ }
+
+ private void validateThatLeafGroupsHasEqualNumberOfNodes() {
+ if (dispatchPolicy != TuningDispatch.DispatchPolicy.ROUNDROBIN) return;
+
+ StorageGroup previousGroup = null;
+ for (StorageGroup group : rootGroup.getSubgroups()) {
+ if (previousGroup == null) { // first group
+ previousGroup = group;
+ continue;
+ }
+
+ if (group.getNodes().size() != previousGroup.getNodes().size())
+ throw new IllegalArgumentException(getErrorMsgPrefix() + "Expected leaf groups to contain an equal number of nodes, but leaf group '" +
+ previousGroup.getName() + "' contains " + previousGroup.getNodes().size() + " node(s) while leaf group '" +
+ group.getName() + "' contains " + group.getNodes().size() + " node(s).");
+ previousGroup = group;
+ }
+ }
+
+ private void validateThatLeafGroupsCountIsAFactorOfRedundancy() {
+ if (redundancy.effectiveFinalRedundancy() % rootGroup.getSubgroups().size() != 0) {
+ throw new IllegalArgumentException(getErrorMsgPrefix() + "Expected number of leaf groups (" +
+ rootGroup.getSubgroups().size() + ") to be a factor of redundancy (" +
+ redundancy.effectiveFinalRedundancy() + "), but it is not.");
+ }
+ }
+
+ private void validateThatRedundancyPerGroupIsEqual() {
+ int redundancyPerGroup = redundancy.effectiveFinalRedundancy() / rootGroup.getSubgroups().size();
+ String expPartitions = createDistributionPartitions(redundancyPerGroup, rootGroup.getSubgroups().size());
+ if (!rootGroup.getPartitions().get().equals(expPartitions)) {
+ throw new IllegalArgumentException(getErrorMsgPrefix() + "Expected redundancy per leaf group to be " +
+ redundancyPerGroup + ", but it is not according to distribution partitions '" +
+ rootGroup.getPartitions().get() + "'. Expected distribution partitions should be '" + expPartitions + "'.");
+ }
+ }
+
+ private List<StorageNode> nonRetired(List<StorageNode> nodes) {
+ return nodes.stream().filter((node) -> { return !node.isRetired(); } ).collect(Collectors.toList());
+ }
+
+ private String createDistributionPartitions(int redundancyPerGroup, int numGroups) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < numGroups - 1; ++i) {
+ sb.append(redundancyPerGroup);
+ sb.append("|");
+ }
+ sb.append("*");
+ return sb.toString();
+ }
+
+ private void validateThatReadyCopiesIsCompatibleWithRedundancy(int groupCount) throws Exception {
+ if (redundancy.effectiveFinalRedundancy() % groupCount != 0) {
+ throw new Exception(getErrorMsgPrefix() + "Expected equal redundancy per group.");
+ }
+ if (redundancy.effectiveReadyCopies() % groupCount != 0) {
+ throw new Exception(getErrorMsgPrefix() + "Expected equal amount of ready copies per group, but " +
+ redundancy.effectiveReadyCopies() + " ready copies is specified with " + groupCount + " groups");
+ }
+ if (redundancy.effectiveReadyCopies() == 0) {
+ System.err.println(getErrorMsgPrefix() + "Warning. No ready copies configured. At least one is recommended.");
+ }
+ }
+
+ private String getErrorMsgPrefix() {
+ return "In indexed content cluster '" + clusterName + "' using hierarchic distribution: ";
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/PriorityMapping.java b/config-model/src/main/java/com/yahoo/vespa/model/content/PriorityMapping.java
new file mode 100644
index 00000000000..597949ec9f2
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/PriorityMapping.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: thomasg
+ * Date: 5/7/12
+ * Time: 2:00 PM
+ * To change this template use File | Settings | File Templates.
+ */
+public class PriorityMapping {
+ ModelElement clusterXml;
+ Map<DocumentProtocol.Priority, Integer> priorityMappings = new HashMap<>();
+
+ public PriorityMapping(ModelElement clusterXml) {
+ this.clusterXml = clusterXml;
+
+ int val = 50;
+ for (DocumentProtocol.Priority p : DocumentProtocol.Priority.values()) {
+ priorityMappings.put(p, val);
+ val += 10;
+ }
+ priorityMappings.put(DocumentProtocol.Priority.HIGHEST, 0);
+ priorityMappings.put(DocumentProtocol.Priority.LOWEST, 255);
+ }
+
+ public int getPriorityMapping(String priorityName) {
+ return priorityMappings.get(Enum.valueOf(DocumentProtocol.Priority.class, priorityName));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/Redundancy.java b/config-model/src/main/java/com/yahoo/vespa/model/content/Redundancy.java
new file mode 100644
index 00000000000..262c985e733
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/Redundancy.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+
+/**
+ * Configuration of the redundancy of a content cluster.
+ *
+ * @author bratseth
+ */
+public class Redundancy implements StorDistributionConfig.Producer {
+
+ private final int initialRedundancy ;
+ private final int finalRedundancy;
+ private final int readyCopies;
+
+ private int implicitGroups = 1;
+
+ /** The total number of nodes available in this cluster (assigned when this becomes known) */
+ private int totalNodes = 0;
+
+ public Redundancy(int initialRedundancy, int finalRedundancy, int readyCopies) {
+ this.initialRedundancy = initialRedundancy;
+ this.finalRedundancy = finalRedundancy;
+ this.readyCopies = readyCopies;
+ }
+
+ /**
+ * Set the total number of nodes available in this cluster.
+ * This impacts the effective redundancy in the case where there are fewer nodes available than
+ * the requested redundancy.
+ */
+ public void setTotalNodes(int totalNodes) { this.totalNodes = totalNodes; }
+
+ /**
+ * Sets the number of groups resulting from implicit setup (groups attribute)
+ * in this cluster. With implicit groups the redundancy settings are taken to be
+ * <i>per group</i> and are multiplied by this number to get the effective <i>total</i>
+ * values returned in the config.
+ */
+ public void setImplicitGroups(int implicitGroups) { this.implicitGroups = implicitGroups; }
+
+ public int initialRedundancy() { return initialRedundancy; }
+ public int finalRedundancy() { return finalRedundancy; }
+ public int readyCopies() { return readyCopies; }
+
+ public int effectiveInitialRedundancy() { return Math.min(totalNodes, initialRedundancy * implicitGroups); }
+ public int effectiveFinalRedundancy() { return Math.min(totalNodes, finalRedundancy * implicitGroups); }
+ public int effectiveReadyCopies() { return Math.min(totalNodes, readyCopies * implicitGroups); }
+
+ @Override
+ public void getConfig(StorDistributionConfig.Builder builder) {
+ builder.initial_redundancy(effectiveInitialRedundancy());
+ builder.redundancy(effectiveFinalRedundancy());
+ builder.ready_copies(effectiveReadyCopies());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java b/config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java
new file mode 100644
index 00000000000..bb16d542c20
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/ResourceLimits.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.util.Optional;
+
+/**
+ * Class tracking resource limits for a content cluster with engine proton.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class ResourceLimits implements ProtonConfig.Producer {
+
+ private final Optional<Double> diskLimit;
+ private final Optional<Double> memoryLimit;
+
+ private ResourceLimits(Builder builder) {
+ this.diskLimit = builder.diskLimit;
+ this.memoryLimit = builder.memoryLimit;
+ }
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ ProtonConfig.Writefilter.Builder writeFilterBuilder = new ProtonConfig.Writefilter.Builder();
+ if (diskLimit.isPresent()) {
+ writeFilterBuilder.disklimit(diskLimit.get());
+ }
+ if (memoryLimit.isPresent()) {
+ writeFilterBuilder.memorylimit(memoryLimit.get());
+ }
+ builder.writefilter(writeFilterBuilder);
+ }
+
+ public static class Builder {
+
+ private Optional<Double> diskLimit = Optional.empty();
+ private Optional<Double> memoryLimit = Optional.empty();
+
+ public ResourceLimits build() {
+ return new ResourceLimits(this);
+ }
+
+ public Builder setDiskLimit(double diskLimit) {
+ this.diskLimit = Optional.of(diskLimit);
+ return this;
+ }
+
+ public Builder setMemoryLimit(double memoryLimit) {
+ this.memoryLimit = Optional.of(memoryLimit);
+ return this;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/SearchCoverage.java b/config-model/src/main/java/com/yahoo/vespa/model/content/SearchCoverage.java
new file mode 100644
index 00000000000..4c93711ebd3
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/SearchCoverage.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class SearchCoverage {
+
+ private final Double minimum;
+ private final Double minWaitAfterCoverageFactor;
+ private final Double maxWaitAfterCoverageFactor;
+
+ private SearchCoverage(Builder builder) {
+ minimum = builder.minimum;
+ minWaitAfterCoverageFactor = builder.minWaitAfterCoverageFactor;
+ maxWaitAfterCoverageFactor = builder.maxWaitAfterCoverageFactor;
+ }
+
+ public Double getMinimum() {
+ return minimum;
+ }
+
+ public Double getMinWaitAfterCoverageFactor() {
+ return minWaitAfterCoverageFactor;
+ }
+
+ public Double getMaxWaitAfterCoverageFactor() {
+ return maxWaitAfterCoverageFactor;
+ }
+
+ public static class Builder {
+
+ private Double minimum;
+ private Double minWaitAfterCoverageFactor;
+ private Double maxWaitAfterCoverageFactor;
+
+ public SearchCoverage build() {
+ return new SearchCoverage(this);
+ }
+
+ public Builder setMinimum(Double value) {
+ Preconditions.checkArgument(value == null || (value >= 0 && value <= 1),
+ "Expected value in range [0, 1], got " + value + ".");
+ minimum = value;
+ return this;
+ }
+
+ public Builder setMinWaitAfterCoverageFactor(Double value) {
+ Preconditions.checkArgument(value == null || (value >= 0 && value <= 1),
+ "Expected value in range [0, 1], got " + value + ".");
+ Preconditions.checkArgument(value == null || maxWaitAfterCoverageFactor == null ||
+ value <= maxWaitAfterCoverageFactor,
+ "Minimum wait (got %s) must be no larger than maximum wait (was %s).",
+ value, maxWaitAfterCoverageFactor);
+ minWaitAfterCoverageFactor = value;
+ return this;
+ }
+
+ public Builder setMaxWaitAfterCoverageFactor(Double value) {
+ Preconditions.checkArgument(value == null || (value >= 0 && value <= 1),
+ "Expected value in range [0, 1], got " + value + ".");
+ Preconditions.checkArgument(value == null || minWaitAfterCoverageFactor == null ||
+ value >= minWaitAfterCoverageFactor,
+ "Maximum wait (got %s) must be no smaller than minimum wait (was %s).",
+ value, minWaitAfterCoverageFactor);
+ maxWaitAfterCoverageFactor = value;
+ return this;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java b/config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java
new file mode 100644
index 00000000000..78255ce8f64
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/StorageGroup.java
@@ -0,0 +1,420 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.NodesSpecification;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.engines.PersistenceEngine;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * A group of storage nodes/distributors.
+ *
+ * @author unknown, probably thomasg
+ * @author bratseth has done things here recently
+ */
+public class StorageGroup {
+
+ private final boolean useCpuSocketAffinity;
+ private final String index;
+ private Optional<String> partitions;
+ String name;
+ private final ContentCluster owner;
+ private final Optional<Long> mmapNoCoreLimit;
+
+ private final List<StorageGroup> subgroups = new ArrayList<>();
+ private final List<StorageNode> nodes = new ArrayList<>();
+
+ /**
+ * Creates a storage group
+ *
+ * @param owner the cluster this group belongs to
+ * @param name the name of this group
+ * @param index the distribution-key index og this group
+ * @param partitions the distribution strategy to use to distribute content to subgroups or empty
+ * (meaning that the "*" distribution will be used) only if this is a leaf group
+ * (having nodes, not subgroups as children).
+ * @param useCpuSocketAffinity whether processes should be started with socket affinity
+ */
+ private StorageGroup(ContentCluster owner, String name, String index, Optional<String> partitions,
+ boolean useCpuSocketAffinity, Optional<Long> mmapNoCoreLimit)
+ {
+ this.owner = owner;
+ this.index = index;
+ this.name = name;
+ this.partitions = partitions;
+ this.useCpuSocketAffinity = useCpuSocketAffinity;
+ this.mmapNoCoreLimit = mmapNoCoreLimit;
+ }
+
+ /** Returns the name of this group, or null if it is the root group */
+ public String getName() { return name; }
+
+ /** Returns the subgroups of this, or an empty list if it is a leaf group */
+ public List<StorageGroup> getSubgroups() { return subgroups; }
+
+ /** Returns the nodes of this, or an empty list of it is not a leaf group */
+ public List<StorageNode> getNodes() { return nodes; }
+
+ public ContentCluster getOwner() { return owner; }
+
+ /** Returns the index of this group, or null if it is the root group */
+ public String getIndex() { return index; }
+
+ public Optional<String> getPartitions() { return partitions; }
+ public boolean useCpuSocketAffinity() { return useCpuSocketAffinity; }
+ public Optional<Long> getMmapNoCoreLimit() { return mmapNoCoreLimit; }
+
+ /** Returns all the nodes below this group */
+ public List<StorageNode> recursiveGetNodes() {
+ if ( ! nodes.isEmpty()) return nodes;
+ List<StorageNode> nodes = new ArrayList<>();
+ for (StorageGroup subgroup : subgroups)
+ nodes.addAll(subgroup.recursiveGetNodes());
+ return nodes;
+ }
+
+ public Collection<StorDistributionConfig.Group.Builder> getGroupStructureConfig() {
+ List<StorDistributionConfig.Group.Builder> groups = new ArrayList<>();
+
+ StorDistributionConfig.Group.Builder myGroup = new StorDistributionConfig.Group.Builder();
+ getConfig(myGroup);
+ groups.add(myGroup);
+
+ for (StorageGroup g : subgroups) {
+ groups.addAll(g.getGroupStructureConfig());
+ }
+
+ return groups;
+ }
+
+ public void getConfig(StorDistributionConfig.Group.Builder builder) {
+ builder.index(index == null ? "invalid" : index);
+ builder.name(name == null ? "invalid" : name);
+ if (partitions.isPresent())
+ builder.partitions(partitions.get());
+ for (StorageNode node : nodes) {
+ StorDistributionConfig.Group.Nodes.Builder nb = new StorDistributionConfig.Group.Nodes.Builder();
+ nb.index(node.getDistributionKey());
+ nb.retired(node.isRetired());
+ builder.nodes.add(nb);
+ }
+ builder.capacity(getCapacity());
+ }
+
+ public int getNumberOfLeafGroups() {
+ int count = subgroups.isEmpty() ? 1 : 0;
+ for (StorageGroup g : subgroups) {
+ count += g.getNumberOfLeafGroups();
+ }
+ return count;
+ }
+
+ public double getCapacity() {
+ double capacity = 0;
+ for (StorageNode node : nodes) {
+ capacity += node.getCapacity();
+ }
+ for (StorageGroup group : subgroups) {
+ capacity += group.getCapacity();
+ }
+ return capacity;
+ }
+
+ /** Returns the total number of nodes below this group */
+ public int countNodes() {
+ int nodeCount = nodes.size();
+ for (StorageGroup group : subgroups)
+ nodeCount += group.countNodes();
+ return nodeCount;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof StorageGroup) {
+ StorageGroup rhs = (StorageGroup)obj;
+ return this.index.equals(rhs.index) &&
+ this.name.equals(rhs.name) &&
+ this.partitions.equals(rhs.partitions);
+ }
+ return false;
+ }
+
+ public static class Builder {
+
+ private final ModelElement clusterElement;
+ private final ContentCluster owner;
+ private final DeployLogger deployLogger;
+
+ public Builder(ModelElement clusterElement, ContentCluster owner, DeployLogger deployLogger) {
+ this.clusterElement = clusterElement;
+ this.owner = owner;
+ this.deployLogger = deployLogger;
+ }
+
+ public StorageGroup buildRootGroup() {
+ Optional<ModelElement> group = Optional.ofNullable(clusterElement.getChild("group"));
+ Optional<ModelElement> nodes = getNodes(clusterElement);
+
+ if (group.isPresent() && nodes.isPresent())
+ throw new IllegalStateException("Both group and nodes exists, only one of these tags is legal");
+ if (group.isPresent() && (group.get().getStringAttribute("name") != null || group.get().getIntegerAttribute("distribution-key") != null))
+ owner.deployLogger().log(LogLevel.INFO, "'distribution-key' attribute on a content cluster's root group is ignored");
+
+ GroupBuilder groupBuilder = collectGroup(group, nodes, null, null);
+ if (owner.isHostedVespa()) {
+ return groupBuilder.buildHosted(owner, Optional.empty());
+ } else {
+ return groupBuilder.buildNonHosted(owner, Optional.empty());
+ }
+ }
+
+ /**
+ * Represents a storage group and can build storage nodes in both hosted and non-hosted environments.
+ */
+ private static class GroupBuilder {
+
+ private final StorageGroup storageGroup;
+
+ /* The explicitly defined subgroups of this */
+ private final List<GroupBuilder> subGroups;
+ private final List<XmlNodeBuilder> nodeBuilders;
+
+ /** The nodes explicitly specified as a nodes tag in this group, or empty if none */
+ private final Optional<NodesSpecification> nodeRequirement;
+
+ private final DeployLogger deployLogger;
+
+ private GroupBuilder(StorageGroup storageGroup, List<GroupBuilder> subGroups, List<XmlNodeBuilder> nodeBuilders,
+ Optional<NodesSpecification> nodeRequirement, DeployLogger deployLogger) {
+ this.storageGroup = storageGroup;
+ this.subGroups = subGroups;
+ this.nodeBuilders = nodeBuilders;
+ this.nodeRequirement = nodeRequirement;
+ this.deployLogger = deployLogger;
+ }
+
+ /**
+ * Builds a storage group for a nonhosted environment
+ *
+ * @param owner the cluster owning this
+ * @param parent the parent storage group, or empty if this is the root group
+ * @return the storage group build by this
+ */
+ public StorageGroup buildNonHosted(ContentCluster owner, Optional<GroupBuilder> parent) {
+ for (GroupBuilder subGroup : subGroups) {
+ storageGroup.subgroups.add(subGroup.buildNonHosted(owner, Optional.of(this)));
+ }
+ for (XmlNodeBuilder nodeBuilder : nodeBuilders) {
+ storageGroup.nodes.add(nodeBuilder.build(owner, storageGroup));
+ }
+
+ if ( ! parent.isPresent())
+ owner.redundancy().setTotalNodes(storageGroup.countNodes());
+
+ return storageGroup;
+ }
+
+ /**
+ * Builds a storage group for a hosted environment
+ *
+ * @param owner the cluster owning this
+ * @param parent the parent storage group, or empty if this is the root group
+ * @return the storage group build by this
+ */
+ public StorageGroup buildHosted(ContentCluster owner, Optional<GroupBuilder> parent) {
+ Map<HostResource, ClusterMembership> hostMapping = nodeRequirement.isPresent() ? allocateHosts(owner) : Collections.emptyMap();
+
+ Map<Optional<ClusterSpec.Group>, Map<HostResource, ClusterMembership>> hostGroups = collectAllocatedSubgroups(hostMapping);
+ if (hostGroups.size() > 1) {
+ if (parent.isPresent())
+ throw new IllegalArgumentException("Cannot specify groups using the groups attribute in nested content groups");
+ owner.redundancy().setTotalNodes(hostMapping.size());
+
+ // Switch redundancy settings to meaning "per group"
+ owner.redundancy().setImplicitGroups(hostGroups.size());
+
+ // Compute partitions expression
+ int redundancyPerGroup = (int)Math.floor(owner.redundancy().effectiveFinalRedundancy() / hostGroups.size());
+ storageGroup.partitions = Optional.of(computePartitions(redundancyPerGroup, hostGroups.size()));
+
+ // create subgroups as returned from allocation
+ for (Map.Entry<Optional<ClusterSpec.Group>, Map<HostResource, ClusterMembership>> hostGroup : hostGroups.entrySet()) {
+ String groupIndex = hostGroup.getKey().get().value();
+ StorageGroup subgroup = new StorageGroup(owner, groupIndex, groupIndex, Optional.empty(), false, Optional.empty());
+ for (Map.Entry<HostResource, ClusterMembership> host : hostGroup.getValue().entrySet()) {
+ subgroup.nodes.add(createStorageNode(owner, host.getKey(), subgroup, host.getValue()));
+ }
+ storageGroup.subgroups.add(subgroup);
+ }
+ }
+ else { // or otherwise just create the nodes directly on this group, or the explicitly enumerated subgroups
+ for (Map.Entry<HostResource, ClusterMembership> host : hostMapping.entrySet()) {
+ storageGroup.nodes.add(createStorageNode(owner, host.getKey(), storageGroup, host.getValue()));
+ }
+ for (GroupBuilder subGroup : subGroups) {
+ storageGroup.subgroups.add(subGroup.buildHosted(owner, Optional.of(this)));
+ }
+ if ( ! parent.isPresent())
+ owner.redundancy().setTotalNodes(storageGroup.countNodes());
+ }
+ return storageGroup;
+ }
+
+ /** This returns a partition string which specifies equal distribution between all groups */
+ // TODO: Make a partitions object
+ private String computePartitions(int redundancyPerGroup, int numGroups) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < numGroups - 1; ++i) {
+ sb.append(redundancyPerGroup);
+ sb.append("|");
+ }
+ sb.append("*");
+ return sb.toString();
+ }
+
+ private Map<HostResource, ClusterMembership> allocateHosts(ContentCluster parent) {
+ ClusterSpec.Id clusterId = ClusterSpec.Id.from(parent.getStorageNodes().getClusterName());
+ Optional<ClusterSpec.Group> groupId = storageGroup.getIndex() == null ? Optional.empty() : Optional.of(ClusterSpec.Group.from(storageGroup.getIndex()));
+ return nodeRequirement.get().provision(parent.getRoot().getHostSystem(), ClusterSpec.Type.content, clusterId, groupId, deployLogger);
+ }
+
+ /** Collect hosts per group */
+ private Map<Optional<ClusterSpec.Group>, Map<HostResource, ClusterMembership>> collectAllocatedSubgroups(Map<HostResource, ClusterMembership> hostMapping) {
+ Map<Optional<ClusterSpec.Group>, Map<HostResource, ClusterMembership>> hostsPerGroup = new LinkedHashMap<>();
+ for (Map.Entry<HostResource, ClusterMembership> entry : hostMapping.entrySet()) {
+ Optional<ClusterSpec.Group> group = entry.getValue().cluster().group();
+ Map<HostResource, ClusterMembership> hostsInGroup = hostsPerGroup.get(group);
+ if (hostsInGroup == null) {
+ hostsInGroup = new LinkedHashMap<>();
+ hostsPerGroup.put(group, hostsInGroup);
+ }
+ hostsInGroup.put(entry.getKey(), entry.getValue());
+ }
+ return hostsPerGroup;
+ }
+
+ }
+
+ private static class XmlNodeBuilder {
+ private final ModelElement clusterElement;
+ private final ModelElement element;
+
+ private XmlNodeBuilder(ModelElement clusterElement, ModelElement element) {
+ this.clusterElement = clusterElement;
+ this.element = element;
+ }
+
+ public StorageNode build(ContentCluster parent, StorageGroup storageGroup) {
+ StorageNode sNode = new StorageNode.Builder().build(parent.getStorageNodes(), element.getXml());
+ PersistenceEngine provider = parent.getPersistence().create(sNode, storageGroup, element);
+ new Distributor.Builder(clusterElement, provider).build(parent.getDistributorNodes(), element.getXml());
+ return sNode;
+ }
+ }
+
+ /**
+ * Creates a content group builder from a group and/or nodes element.
+ * These are the possibilities:
+ * <ul>
+ * <li>group and nodes is present: This is a leaf group specifying a set of nodes</li>
+ * <li>only group is present: This is a nonleaf group</li>
+ * <li>only nodes is present: This is the implicitly specified toplevel leaf group, or a set of groups
+ * specified using a group count attrbute.
+ * </ul>
+ */
+ private GroupBuilder collectGroup(Optional<ModelElement> groupElement, Optional<ModelElement> nodesElement, String name, String index) {
+ StorageGroup group = new StorageGroup(owner, name, index,
+ childAsString(groupElement, "distribution.partitions"),
+ booleanAttributeOr(groupElement, VespaDomBuilder.CPU_SOCKET_AFFINITY_ATTRIB_NAME, false),
+ childAsLong(groupElement, VespaDomBuilder.MMAP_NOCORE_LIMIT));
+
+ List<GroupBuilder> subGroups = groupElement.isPresent() ? collectSubGroups(group, groupElement.get()) : Collections.emptyList();
+
+ List<XmlNodeBuilder> explicitNodes = new ArrayList<>();
+ explicitNodes.addAll(collectExplicitNodes(groupElement));
+ explicitNodes.addAll(collectExplicitNodes(nodesElement));
+
+ if (subGroups.size() > 0 && explicitNodes.size() > 0)
+ throw new IllegalArgumentException("A group can contain either nodes or groups, but not both.");
+
+ Optional<NodesSpecification> nodeRequirement =
+ nodesElement.isPresent() && nodesElement.get().getStringAttribute("count") != null ? Optional.of(NodesSpecification.from(nodesElement.get())) : Optional.empty();
+ if (nodeRequirement.isPresent() && subGroups.size() > 0)
+ throw new IllegalArgumentException("A group can contain either explicit subgroups or a nodes specification, but not both.");
+ return new GroupBuilder(group, subGroups, explicitNodes, nodeRequirement, deployLogger);
+ }
+
+ private Optional<String> childAsString(Optional<ModelElement> element, String childTagName) {
+ if ( ! element.isPresent()) return Optional.empty();
+ return Optional.ofNullable(element.get().childAsString(childTagName));
+ }
+ private Optional<Long> childAsLong(Optional<ModelElement> element, String childTagName) {
+ if ( ! element.isPresent()) return Optional.empty();
+ return Optional.ofNullable(element.get().childAsLong(childTagName));
+ }
+
+ private boolean booleanAttributeOr(Optional<ModelElement> element, String attributeName, boolean defaultValue) {
+ if ( ! element.isPresent()) return defaultValue;
+ return element.get().getBooleanAttribute(attributeName, defaultValue);
+ }
+
+ private Optional<ModelElement> getNodes(ModelElement groupOrNodesElement) {
+ if (groupOrNodesElement.getXml().getTagName().equals("nodes")) return Optional.of(groupOrNodesElement);
+ return Optional.ofNullable(groupOrNodesElement.getChild("nodes"));
+ }
+
+ private List<XmlNodeBuilder> collectExplicitNodes(Optional<ModelElement> groupOrNodesElement) {
+ if ( ! groupOrNodesElement.isPresent()) return Collections.emptyList();
+ List<XmlNodeBuilder> nodes = new ArrayList<>();
+ for (ModelElement n : groupOrNodesElement.get().subElements("node"))
+ nodes.add(new XmlNodeBuilder(clusterElement, n));
+ return nodes;
+ }
+
+ private List<GroupBuilder> collectSubGroups(StorageGroup parentGroup, ModelElement parentGroupElement) {
+ List<ModelElement> subGroupElements = parentGroupElement.subElements("group");
+ if (subGroupElements.size() > 1 && ! parentGroup.getPartitions().isPresent())
+ throw new IllegalArgumentException("'distribution' attribute is required with multiple subgroups");
+
+ List<GroupBuilder> subGroups = new ArrayList<>();
+ String indexPrefix = "";
+ if (parentGroup.index != null) {
+ indexPrefix = parentGroup.index + ".";
+ }
+ for (ModelElement g : subGroupElements) {
+ subGroups.add(collectGroup(Optional.of(g), Optional.ofNullable(g.getChild("nodes")), g.getStringAttribute("name"),
+ indexPrefix + g.getIntegerAttribute("distribution-key")));
+ }
+ return subGroups;
+ }
+
+ private static StorageNode createStorageNode(ContentCluster parent, HostResource hostResource, StorageGroup parentGroup, ClusterMembership clusterMembership) {
+ StorageNode sNode = new StorageNode(parent.getStorageNodes(), null, clusterMembership.index(), clusterMembership.retired());
+ sNode.setHostResource(hostResource);
+ sNode.initService();
+
+ // TODO: Supplying null as XML is not very nice
+ PersistenceEngine provider = parent.getPersistence().create(sNode, parentGroup, null);
+ Distributor d = new Distributor(parent.getDistributorNodes(), clusterMembership.index(), null, provider);
+ d.setHostResource(sNode.getHostResource());
+ d.initService();
+ return sNode;
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/StorageNode.java b/config-model/src/main/java/com/yahoo/vespa/model/content/StorageNode.java
new file mode 100644
index 00000000000..750a66a0dda
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/StorageNode.java
@@ -0,0 +1,116 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.StorFilestorConfig;
+import com.yahoo.vespa.config.content.core.StorBucketmoverConfig;
+import com.yahoo.vespa.config.storage.StorDevicesConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.config.storage.StorMemfilepersistenceConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.content.engines.PersistenceEngine;
+import com.yahoo.vespa.model.content.engines.ProtonProvider;
+import com.yahoo.vespa.model.content.storagecluster.StorageCluster;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+
+/**
+ * Class to provide config related to a specific storage node.
+ */
+@RestartConfigs({StorDevicesConfig.class, StorFilestorConfig.class,
+ StorMemfilepersistenceConfig.class, StorBucketmoverConfig.class})
+public class StorageNode extends ContentNode implements StorServerConfig.Producer, StorDevicesConfig.Producer {
+
+ static final String rootFolder = Defaults.getDefaults().vespaHome() + "var/db/vespa/vds/";
+
+ private final Double capacity;
+ private final boolean retired;
+ private final boolean isHostedVespa;
+ private boolean usesVdsEngine = false;
+
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<StorageNode> {
+ @Override
+ protected StorageNode doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ ModelElement e = new ModelElement(producerSpec);
+ return new StorageNode((StorageCluster)ancestor, e.getDoubleAttribute("capacity"), e.getIntegerAttribute("distribution-key"), false);
+ }
+ }
+
+ StorageNode(StorageCluster cluster, Double capacity, int distributionKey, boolean retired) {
+ super(cluster,
+ cluster.getClusterName(),
+ rootFolder + cluster.getClusterName() + "/storage/" + distributionKey,
+ distributionKey);
+ this.retired = retired;
+ this.capacity = capacity;
+ this.isHostedVespa = cluster.getRoot().getDeployState().getProperties().hostedVespa();
+ }
+
+ @Override
+ public String getStartupCommand() {
+ return isProviderProton()
+ ? null
+ : "exec sbin/storaged -c $VESPA_CONFIG_ID";
+ }
+
+ @Override
+ public void getConfig(StorDevicesConfig.Builder builder) {
+ String root_folder = getRootDirectory();
+ builder.root_folder(root_folder);
+
+ // For VDS in hosted Vespa, we default to using the root_folder as the disk to store the data in.
+ // Setting disk_path will then
+ if (isHostedVespa && usesVdsEngine) {
+ // VDS looks up the first disk at the directory path root_folder/disks/d0.
+ builder.disk_path(Arrays.asList(root_folder + "/disks/d0"));
+ }
+ }
+
+ // 2015-08-11: Needed because of the following circular dependency:
+ // 1. StorageNode is created.
+ // 2. A particular persistence engine is picked depending on things (like the presence of engine/vds element)
+ // that are hidden from the code creating the StorageNode in (1).
+ // 3. The persistence engine depends on the StorageNode, e.g. it's a parent node.
+ //
+ // If the VDSEngine is picked in (2), we would like to know this in StorageNode::getConfig(). Hence this setter.
+ public void useVdsEngine() {
+ usesVdsEngine = true;
+ }
+
+
+ public double getCapacity() {
+ if (capacity != null) {
+ return capacity;
+ } else {
+ return 1.0;
+ }
+ }
+
+ /** Whether this node is configured as retired, which means all content should migrate off the node */
+ public boolean isRetired() { return retired; }
+
+ private boolean isProviderProton() {
+ for (AbstractConfigProducer producer : getChildren().values()) {
+ if (producer instanceof ProtonProvider) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ super.getConfig(builder);
+
+ builder.node_capacity(getCapacity());
+
+ for (AbstractConfigProducer producer : getChildren().values()) {
+ ((PersistenceEngine)producer).getConfig(builder);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/TuningDispatch.java b/config-model/src/main/java/com/yahoo/vespa/model/content/TuningDispatch.java
new file mode 100644
index 00000000000..614be479d38
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/TuningDispatch.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class TuningDispatch {
+
+ private final Integer maxHitsPerPartition;
+ public static enum DispatchPolicy { ROUNDROBIN, RANDOM};
+ private final DispatchPolicy dispatchPolicy;
+ private final Boolean useLocalNode;
+ private final Double minGroupCoverage;
+ private final Double minActiveDocsCoverage;
+
+ private TuningDispatch(Builder builder)
+ {
+ maxHitsPerPartition = builder.maxHitsPerPartition;
+ dispatchPolicy = builder.dispatchPolicy;
+ useLocalNode = builder.useLocalNode;
+ minGroupCoverage = builder.minGroupCoverage;
+ minActiveDocsCoverage = builder.minActiveDocsCoverage;
+ }
+
+ public Integer getMaxHitsPerPartition() {
+ return maxHitsPerPartition;
+ }
+ public DispatchPolicy getDispatchPolicy() { return dispatchPolicy; }
+ public Boolean getUseLocalNode() { return useLocalNode; }
+ public Double getMinGroupCoverage() { return minGroupCoverage; }
+ public Double getMinActiveDocsCoverage() { return minActiveDocsCoverage; }
+
+ public static class Builder {
+
+ private Integer maxHitsPerPartition;
+ private DispatchPolicy dispatchPolicy = DispatchPolicy.ROUNDROBIN;
+ private Boolean useLocalNode;
+ private Double minGroupCoverage;
+ private Double minActiveDocsCoverage;
+
+ public TuningDispatch build() {
+ return new TuningDispatch(this);
+ }
+
+ public Builder setMaxHitsPerPartition(Integer maxHitsPerPartition) {
+ this.maxHitsPerPartition = maxHitsPerPartition;
+ return this;
+ }
+ public Builder setDispatchPolicy(String policy) {
+ if (policy == null) {
+ } else if ("round-robin".equals(policy.toLowerCase())) {
+ dispatchPolicy = DispatchPolicy.ROUNDROBIN;
+ } else {
+ dispatchPolicy = DispatchPolicy.valueOf(policy.toUpperCase());
+ }
+ return this;
+ }
+
+ public Builder setUseLocalNode(Boolean useLocalNode) {
+ this.useLocalNode = useLocalNode;
+ return this;
+ }
+ public Builder setMinGroupCoverage(Double minGroupCoverage) {
+ this.minGroupCoverage = minGroupCoverage;
+ return this;
+ }
+ public Builder setMinActiveDocsCoverage(Double minCoverage) {
+ this.minActiveDocsCoverage = minCoverage;
+ return this;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java
new file mode 100644
index 00000000000..08685659d35
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/ContentCluster.java
@@ -0,0 +1,604 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
+import com.yahoo.config.model.ConfigModelUtils;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.config.provision.ClusterSpec;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.vespa.config.content.MessagetyperouteselectorpolicyConfig;
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+import com.yahoo.vespa.config.content.core.StorDistributormanagerConfig;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol;
+import com.yahoo.metrics.MetricsmanagerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.admin.Metric;
+import com.yahoo.vespa.model.admin.MetricsConsumer;
+import com.yahoo.vespa.model.admin.MonitoringSystem;
+import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerCluster;
+import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerComponent;
+import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerConfigurer;
+import com.yahoo.vespa.model.admin.clustercontroller.ClusterControllerContainer;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.builder.xml.dom.NodesSpecification;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespa.model.content.*;
+import com.yahoo.vespa.model.content.engines.PersistenceEngine;
+import com.yahoo.vespa.model.content.engines.ProtonEngine;
+import com.yahoo.vespa.model.content.engines.VDSEngine;
+import com.yahoo.vespa.model.content.storagecluster.StorageCluster;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import com.yahoo.vespa.model.search.MultilevelDispatchValidator;
+import com.yahoo.vespa.model.search.Tuning;
+import org.w3c.dom.Element;
+
+import java.util.*;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+/**
+ * A content cluster.
+ *
+ * @author mostly somebody unknown
+ * @author bratseth
+ */
+public class ContentCluster extends AbstractConfigProducer implements StorDistributionConfig.Producer,
+ StorDistributormanagerConfig.Producer,
+ FleetcontrollerConfig.Producer,
+ MetricsmanagerConfig.Producer,
+ MessagetyperouteselectorpolicyConfig.Producer {
+
+ // TODO: Make private
+ private String documentSelection;
+ ContentSearchCluster search;
+ final Map<String, NewDocumentType> documentDefinitions;
+ com.yahoo.vespa.model.content.StorageGroup rootGroup;
+ StorageCluster storageNodes;
+ DistributorCluster distributorNodes;
+ private Redundancy redundancy;
+ ClusterControllerConfig clusterControllerConfig;
+ PersistenceEngine.PersistenceFactory persistenceFactory;
+ String clusterName;
+ Integer maxNodesPerMerge;
+
+ /**
+ * If multitenant or a cluster controller was explicitly configured in this cluster:
+ * The cluster controller cluster of this particular content cluster.
+ *
+ * Otherwise: null - the cluster controller is shared by all content clusters and part of Admin.
+ */
+ private ContainerCluster clusterControllers;
+
+ public enum DistributionMode { LEGACY, STRICT, LOOSE }
+ DistributionMode distributionMode;
+
+ public static class Builder {
+
+ /** The admin model of this system or null if none (which only happens in tests) */
+ private final Admin admin;
+ private final DeployLogger deployLogger;
+
+ public Builder(Admin admin, DeployLogger deployLogger) {
+ this.admin = admin;
+ this.deployLogger = deployLogger;
+ }
+
+ public ContentCluster build(AbstractConfigProducer ancestor, Element w3cContentElement) {
+ ModelElement contentElement = new ModelElement(w3cContentElement);
+
+ ModelElement documentsElement = contentElement.getChild("documents");
+ Map<String, NewDocumentType> documentDefinitions =
+ new SearchDefinitionBuilder().build(ancestor.getRoot().getDeployState().getDocumentModel().getDocumentManager(), documentsElement);
+
+ String routingSelection = new DocumentSelectionBuilder().build(contentElement.getChild("documents"));
+ Redundancy redundancy = new RedundancyBuilder().build(contentElement);
+
+ ContentCluster c = new ContentCluster(ancestor, getClusterName(contentElement), documentDefinitions,
+ routingSelection, redundancy);
+ c.clusterControllerConfig = new ClusterControllerConfig.Builder(getClusterName(contentElement), contentElement).build(c, contentElement.getXml());
+ c.search = new ContentSearchCluster.Builder(documentDefinitions).build(c, contentElement.getXml());
+ c.persistenceFactory = new EngineFactoryBuilder().build(contentElement, c);
+ c.storageNodes = new StorageCluster.Builder().build(c, w3cContentElement);
+ c.distributorNodes = new DistributorCluster.Builder(c).build(c, w3cContentElement);
+ c.rootGroup = new StorageGroup.Builder(contentElement, c, deployLogger).buildRootGroup();
+ validateThatGroupSiblingsAreUnique(c.clusterName, c.rootGroup);
+ c.search.handleRedundancy(redundancy);
+
+ IndexedSearchCluster index = c.search.getIndexed();
+ if (index != null) {
+ setupIndexedCluster(index, contentElement);
+ }
+
+ if (c.search.hasIndexedCluster() && !(c.persistenceFactory instanceof ProtonEngine.Factory) ) {
+ throw new RuntimeException("If you have indexed search you need to have proton as engine");
+ }
+
+ if (documentsElement != null) {
+ ModelElement e = documentsElement.getChild("document-processing");
+ if (e != null) {
+ setupDocumentProcessing(c, e);
+ }
+ } else if (c.persistenceFactory != null) {
+ throw new IllegalArgumentException("The specified content engine requires the <documents> element to be specified.");
+ }
+
+ ModelElement tuning = contentElement.getChild("tuning");
+ if (tuning != null) {
+ setupTuning(c, tuning);
+ }
+
+ AbstractConfigProducerRoot root = ancestor.getRoot();
+ if (root == null) return c;
+
+ addClusterControllers(root, c.rootGroup, contentElement, c.clusterName, c);
+ return c;
+ }
+
+ private void setupIndexedCluster(IndexedSearchCluster index, ModelElement element) {
+ ContentSearch search = DomContentSearchBuilder.build(element);
+ Double queryTimeout = search.getQueryTimeout();
+ if (queryTimeout != null) {
+ Preconditions.checkState(index.getQueryTimeout() == null,
+ "You may not specify query-timeout in both proton and content.");
+ index.setQueryTimeout(queryTimeout);
+ }
+ Double visibilityDelay = search.getVisibilityDelay();
+ if (visibilityDelay != null) {
+ index.setVisibilityDelay(visibilityDelay);
+ }
+ index.setSearchCoverage(DomSearchCoverageBuilder.build(element));
+ index.setDispatchSpec(DomDispatchBuilder.build(element));
+ if (index.useMultilevelDispatchSetup()) {
+ // We must validate this before we add tlds and setup the dispatch groups.
+ // This must therefore happen before the regular validate() step.
+ new MultilevelDispatchValidator(index.getClusterName(), index.getDispatchSpec(), index.getSearchNodes()).validate();
+ }
+
+ // TODO: This should be cleaned up to avoid having to change code in 100 places
+ // every time we add a dispatch option.
+ TuningDispatch tuningDispatch = DomTuningDispatchBuilder.build(element);
+ Integer maxHitsPerPartition = tuningDispatch.getMaxHitsPerPartition();
+ Boolean useLocalNode = tuningDispatch.getUseLocalNode();
+
+ if (index.getTuning() == null) {
+ index.setTuning(new Tuning(index));
+ }
+ if (index.getTuning().dispatch == null) {
+ index.getTuning().dispatch = new Tuning.Dispatch();
+ }
+ if (maxHitsPerPartition != null) {
+ index.getTuning().dispatch.maxHitsPerPartition = maxHitsPerPartition;
+ }
+ if (useLocalNode != null) {
+ index.getTuning().dispatch.useLocalNode = useLocalNode;
+ }
+ index.getTuning().dispatch.minGroupCoverage = tuningDispatch.getMinGroupCoverage();
+ index.getTuning().dispatch.minActiveDocsCoverage = tuningDispatch.getMinActiveDocsCoverage();
+ index.getTuning().dispatch.policy = tuningDispatch.getDispatchPolicy();
+ }
+
+ private void setupDocumentProcessing(ContentCluster c, ModelElement e) {
+ String docprocCluster = e.getStringAttribute("cluster");
+ if (docprocCluster != null) {
+ docprocCluster = docprocCluster.trim();
+ }
+ if (c.getSearch().hasIndexedCluster()) {
+ if (docprocCluster != null && !docprocCluster.isEmpty()) {
+ c.getSearch().getIndexed().setIndexingClusterName(docprocCluster);
+ }
+ }
+
+ String docprocChain = e.getStringAttribute("chain");
+ if (docprocChain != null) {
+ docprocChain = docprocChain.trim();
+ }
+ if (c.getSearch().hasIndexedCluster()) {
+ if (docprocChain != null && !docprocChain.isEmpty()) {
+ c.getSearch().getIndexed().setIndexingChainName(docprocChain);
+ }
+ }
+ }
+
+ private void setupTuning(ContentCluster c, ModelElement tuning) {
+ ModelElement distribution = tuning.getChild("distribution");
+ if (distribution != null) {
+ String attr = distribution.getStringAttribute("type");
+ if (attr != null) {
+ if (attr.toLowerCase().equals("strict")) {
+ c.distributionMode = DistributionMode.STRICT;
+ } else if (attr.toLowerCase().equals("loose")) {
+ c.distributionMode = DistributionMode.LOOSE;
+ } else if (attr.toLowerCase().equals("legacy")) {
+ c.distributionMode = DistributionMode.LEGACY;
+ } else {
+ throw new IllegalStateException("Distribution type " + attr + " not supported.");
+ }
+ }
+ }
+ ModelElement merges = tuning.getChild("merges");
+ if (merges != null) {
+ Integer attr = merges.getIntegerAttribute("max-nodes-per-merge");
+ if (attr != null) {
+ c.maxNodesPerMerge = attr;
+ }
+ }
+ }
+
+ private void validateGroupSiblings(String cluster, StorageGroup group) {
+ HashSet<String> siblings = new HashSet<>();
+ for (StorageGroup g : group.getSubgroups()) {
+ String name = g.getName();
+ if (siblings.contains(name)) {
+ throw new IllegalArgumentException("Cluster '" + cluster + "' has multiple groups " +
+ "with name '" + name + "' in the same subgroup. Group sibling names must be unique.");
+ }
+ siblings.add(name);
+ }
+ }
+
+ private void validateThatGroupSiblingsAreUnique(String cluster, StorageGroup group) {
+ if (group == null) {
+ return; // Unit testing case
+ }
+ validateGroupSiblings(cluster, group);
+ for (StorageGroup g : group.getSubgroups()) {
+ validateThatGroupSiblingsAreUnique(cluster, g);
+ }
+ }
+
+ private void addClusterControllers(AbstractConfigProducerRoot root, StorageGroup rootGroup, ModelElement contentElement, String contentClusterName, ContentCluster contentCluster) {
+ if (admin == null) return; // only in tests
+ if (contentCluster.getPersistence() == null) return;
+
+ ContainerCluster clusterControllers;
+
+ ContentCluster overlappingCluster = findOverlappingCluster(root, contentCluster);
+ if (overlappingCluster != null && overlappingCluster.getClusterControllers() != null) {
+ // Borrow the cluster controllers of the other cluster in this case.
+ // This condition only obtains on non-hosted systems with a shared config server,
+ // a combination which only exists in system tests
+ clusterControllers = overlappingCluster.getClusterControllers();
+ }
+ else if (admin.multitenant()) {
+ String clusterName = contentClusterName + "-controllers";
+ NodesSpecification nodesSpecification =
+ NodesSpecification.optionalDedicatedFromParent(contentElement.getChild("controllers")).orElse(NodesSpecification.nonDedicated(3));
+ Collection<HostResource> hosts = nodesSpecification.isDedicated() ?
+ getControllerHosts(nodesSpecification, admin, clusterName) :
+ drawContentHosts(nodesSpecification.count(), rootGroup);
+
+ clusterControllers = createClusterControllers(new ClusterControllerCluster(contentCluster, "standalone"), hosts, clusterName, true);
+ contentCluster.clusterControllers = clusterControllers;
+ }
+ else {
+ clusterControllers = admin.getClusterControllers();
+ if (clusterControllers == null) {
+ List<HostResource> hosts = admin.getClusterControllerHosts();
+ if (hosts.size() > 1) {
+ admin.deployLogger().log(Level.INFO, "When having content cluster(s) and more than 1 config server it is recommended to configure cluster controllers explicitly." +
+ " See " + ConfigModelUtils.createDocLink("reference/services-admin.html#cluster-controller"));
+ }
+ clusterControllers = createClusterControllers(admin, hosts, "cluster-controllers", false);
+ admin.setClusterControllers(clusterControllers);
+ }
+ }
+
+ addClusterControllerComponentsForThisCluster(clusterControllers, contentCluster);
+ }
+
+ /** Returns any other content cluster which shares nodes with this, or null if none are built */
+ private ContentCluster findOverlappingCluster(AbstractConfigProducerRoot root, ContentCluster contentCluster) {
+ for (ContentCluster otherContentCluster : root.getChildrenByTypeRecursive(ContentCluster.class)) {
+ if (otherContentCluster != contentCluster && overlaps(contentCluster, otherContentCluster))
+ return otherContentCluster;
+ }
+ return null;
+ }
+
+ private boolean overlaps(ContentCluster c1, ContentCluster c2) {
+ Set<HostResource> c1Hosts = c1.getRootGroup().recursiveGetNodes().stream().map(StorageNode::getHostResource).collect(Collectors.toSet());
+ Set<HostResource> c2Hosts = c2.getRootGroup().recursiveGetNodes().stream().map(StorageNode::getHostResource).collect(Collectors.toSet());
+ return ! Sets.intersection(c1Hosts, c2Hosts).isEmpty();
+ }
+
+ private Collection<HostResource> getControllerHosts(NodesSpecification nodesSpecification, Admin admin, String clusterName) {
+ return nodesSpecification.provision(admin.getHostSystem(), ClusterSpec.Type.admin, ClusterSpec.Id.from(clusterName), Optional.empty(), deployLogger).keySet();
+ }
+
+ private List<HostResource> drawContentHosts(int count, StorageGroup rootGroup) {
+ List<HostResource> hosts = drawContentHostsRecursively(count, rootGroup);
+ if (hosts.size() % 2 == 0) // ZK clusters of even sizes are less available (even in the size=2 case)
+ hosts = hosts.subList(0, hosts.size()-1);
+ return hosts;
+ }
+
+ /**
+ * Draw <code>count</code> nodes from as many different content groups below this as possible.
+ * This will only achieve maximum spread in the case where the groups are balanced and never on the same
+ * physical node. It will not achieve maximum spread over all levels in a multilevel group hierarchy.
+ */
+ // Note: This method cannot be changed to draw different nodes without ensuring that it will draw nodes
+ // which overlaps with previously drawn nodes as this will prevent rolling upgrade
+ private List<HostResource> drawContentHostsRecursively(int count, StorageGroup group) {
+ Set<HostResource> hosts = new HashSet<>();
+ if (group.getNodes().isEmpty()) {
+ int hostsPerSubgroup = (int)Math.ceil((double)count / group.getSubgroups().size());
+ for (StorageGroup subgroup : group.getSubgroups())
+ hosts.addAll(drawContentHostsRecursively(hostsPerSubgroup, subgroup));
+ }
+ else {
+ hosts.addAll(group.getNodes().stream()
+ .filter(node -> ! node.isRetired()) // Avoid retired controllers to avoid surprises on expiry
+ .map(StorageNode::getHostResource).collect(Collectors.toList()));
+ }
+ List<HostResource> sortedHosts = new ArrayList<>(hosts);
+ Collections.sort(sortedHosts);
+ sortedHosts = sortedHosts.subList(0, Math.min(count, hosts.size()));
+ return sortedHosts;
+ }
+
+ private ContainerCluster createClusterControllers(AbstractConfigProducer parent, Collection<HostResource> hosts, String name, boolean multitenant) {
+ ContainerCluster clusterControllers = new ContainerCluster(parent, name, name);
+ List<Container> containers = new ArrayList<>();
+ // Add a cluster controller on each config server (there is always at least one).
+ if (clusterControllers.getContainers().isEmpty()) {
+ int index = 0;
+ for (HostResource host : hosts) {
+ ClusterControllerContainer clusterControllerContainer = new ClusterControllerContainer(clusterControllers, index, multitenant);
+ clusterControllerContainer.setHostResource(host);
+ clusterControllerContainer.initService();
+ clusterControllerContainer.setProp("clustertype", "admin")
+ .setProp("clustername", clusterControllers.getName())
+ .setProp("index", String.valueOf(index));
+ containers.add(clusterControllerContainer);
+ ++index;
+ }
+ }
+ clusterControllers.addContainers(containers);
+ ContainerModelBuilder.addDefaultHandler_legacyBuilder(clusterControllers);
+ return clusterControllers;
+ }
+
+ private void addClusterControllerComponentsForThisCluster(ContainerCluster clusterControllers, ContentCluster contentCluster) {
+ int index = 0;
+ for (Container container : clusterControllers.getContainers()) {
+ if ( ! hasClusterControllerComponent(container))
+ container.addComponent(new ClusterControllerComponent());
+ container.addComponent(new ClusterControllerConfigurer(contentCluster, index++, clusterControllers.getContainers().size()));
+ }
+
+ }
+
+ private boolean hasClusterControllerComponent(Container container) {
+ for (Object o : container.getComponents().getComponents())
+ if (o instanceof ClusterControllerComponent) return true;
+ return false;
+ }
+
+ }
+
+ private ContentCluster(AbstractConfigProducer parent,
+ String clusterName,
+ Map<String, NewDocumentType> documentDefinitions,
+ String routingSelection,
+ Redundancy redundancy) {
+ super(parent, clusterName);
+ this.clusterName = clusterName;
+ this.documentDefinitions = documentDefinitions;
+ this.documentSelection = routingSelection;
+ this.redundancy = redundancy;
+ }
+
+ public void prepare() {
+ search.prepare();
+
+ if (clusterControllers != null) {
+ clusterControllers.prepare();
+ }
+ }
+
+ /** Returns cluster controllers if this is multitenant, null otherwise */
+ public ContainerCluster getClusterControllers() { return clusterControllers; }
+
+ public DistributionMode getDistributionMode() {
+ if (distributionMode != null) return distributionMode;
+ return getPersistence().getDefaultDistributionMode();
+ }
+
+ public boolean isMemfilePersistence() {
+ return persistenceFactory instanceof VDSEngine.Factory;
+ }
+
+ public static String getClusterName(ModelElement clusterElem) {
+ String clusterName = clusterElem.getStringAttribute("id");
+ if (clusterName == null) {
+ clusterName = "content";
+ }
+
+ return clusterName;
+ }
+
+ public String getName() { return clusterName; }
+
+ public String getRoutingSelector() { return documentSelection; }
+
+ public DistributorCluster getDistributorNodes() { return distributorNodes; }
+
+ public StorageCluster getStorageNodes() { return storageNodes; }
+
+ public ClusterControllerConfig getClusterControllerConfig() { return clusterControllerConfig; }
+
+ public PersistenceEngine.PersistenceFactory getPersistence() { return persistenceFactory; }
+
+ /**
+ * The list of documentdefinitions declared at the cluster level.
+ * @return the set of documenttype names
+ */
+ public Map<String, NewDocumentType> getDocumentDefinitions() { return documentDefinitions; }
+
+ public final ContentSearchCluster getSearch() { return search; }
+
+ public Redundancy redundancy() { return redundancy; }
+
+ @Override
+ public void getConfig(MessagetyperouteselectorpolicyConfig.Builder builder) {
+ if (!getSearch().hasIndexedCluster()) return;
+ builder.
+ defaultroute(com.yahoo.vespa.model.routing.DocumentProtocol.getDirectRouteName(getConfigId())).
+ route(new MessagetyperouteselectorpolicyConfig.Route.Builder().
+ messagetype(DocumentProtocol.MESSAGE_PUTDOCUMENT).
+ name(com.yahoo.vespa.model.routing.DocumentProtocol.getIndexedRouteName(getConfigId()))).
+ route(new MessagetyperouteselectorpolicyConfig.Route.Builder().
+ messagetype(DocumentProtocol.MESSAGE_REMOVEDOCUMENT).
+ name(com.yahoo.vespa.model.routing.DocumentProtocol.getIndexedRouteName(getConfigId()))).
+ route(new MessagetyperouteselectorpolicyConfig.Route.Builder().
+ messagetype(DocumentProtocol.MESSAGE_UPDATEDOCUMENT).
+ name(com.yahoo.vespa.model.routing.DocumentProtocol.getIndexedRouteName(getConfigId())));
+ }
+
+ public com.yahoo.vespa.model.content.StorageGroup getRootGroup() {
+ return rootGroup;
+ }
+
+ @Override
+ public void getConfig(StorDistributionConfig.Builder builder) {
+ if (rootGroup != null) {
+ builder.group.addAll(rootGroup.getGroupStructureConfig());
+ }
+
+ if (redundancy != null) {
+ redundancy.getConfig(builder);
+ }
+
+ if (search.usesHierarchicDistribution()) {
+ builder.active_per_leaf_group(true);
+ }
+ }
+
+ int getNodeCount() {
+ return storageNodes.getChildren().size();
+ }
+
+ int getNodeCountPerGroup() {
+ return rootGroup != null ? getNodeCount() / rootGroup.getNumberOfLeafGroups() : getNodeCount();
+ }
+
+ @Override
+ public void getConfig(FleetcontrollerConfig.Builder builder) {
+ builder.ideal_distribution_bits(distributionBits());
+ if (getNodeCount() < 5) {
+ builder.min_storage_up_count(1);
+ builder.min_distributor_up_ratio(0);
+ builder.min_storage_up_ratio(0);
+ }
+ }
+
+ @Override
+ public void getConfig(StorDistributormanagerConfig.Builder builder) {
+ builder.minsplitcount(distributionBits());
+ if (maxNodesPerMerge != null) {
+ builder.maximum_nodes_per_merge(maxNodesPerMerge);
+ }
+ }
+
+ /**
+ * Returns the distribution bits this cluster should use.
+ * OnHosted Vespa this is hardcoded not computed from the nodes because reducing the number of nodes is a common
+ * operation while reducing the number of distribution bits can lead to consistency problems.
+ * This hardcoded value should work fine from 1-200 nodes. Those who have more will need to set this value
+ * in config and not remove it again if they reduce the node count.
+ */
+ public int distributionBits() {
+ // if (hostedVespa) return 16; TODO: Re-enable this later (Nov 2015, ref VESPA-1702)
+ return DistributionBitCalculator.getDistributionBits(getNodeCountPerGroup(), getDistributionMode());
+ }
+
+ @Override
+ public void validate() throws Exception {
+ super.validate();
+ if (search.usesHierarchicDistribution() && ! isHostedVespa()) {
+ // validate manually configured groups
+ new IndexedHierarchicDistributionValidator(search.getClusterName(), rootGroup, redundancy, search.getIndexed().getTuning().dispatch.policy).validate();
+ if (search.getIndexed().useMultilevelDispatchSetup()) {
+ throw new IllegalArgumentException("In indexed content cluster '" + search.getClusterName() + "': Using multi-level dispatch setup is not supported when using hierarchical distribution.");
+ }
+ }
+ }
+
+ public static Map<String, Integer> METRIC_INDEX_MAP = new TreeMap<>();
+ static {
+ METRIC_INDEX_MAP.put("status", 0);
+ METRIC_INDEX_MAP.put("log", 1);
+ METRIC_INDEX_MAP.put("yamas", 2);
+ METRIC_INDEX_MAP.put("health", 3);
+ METRIC_INDEX_MAP.put("fleetcontroller", 4);
+ METRIC_INDEX_MAP.put("statereporter", 5);
+ }
+
+ public static MetricsmanagerConfig.Consumer.Builder getMetricBuilder(String name, MetricsmanagerConfig.Builder builder) {
+ Integer index = METRIC_INDEX_MAP.get(name);
+ if (index != null) {
+ return builder.consumer.get(index);
+ }
+
+ MetricsmanagerConfig.Consumer.Builder retVal = new MetricsmanagerConfig.Consumer.Builder();
+ retVal.name(name);
+ builder.consumer(retVal);
+ return retVal;
+ }
+
+ @Override
+ public void getConfig(MetricsmanagerConfig.Builder builder) {
+ MonitoringSystem monitoringSystem = getMonitoringService();
+ if (monitoringSystem != null) {
+ builder.snapshot(new MetricsmanagerConfig.Snapshot.Builder().
+ periods(monitoringSystem.getIntervalSeconds()).periods(300));
+ }
+ builder.consumer(
+ new MetricsmanagerConfig.Consumer.Builder().
+ name("status").
+ addedmetrics("*").
+ removedtags("partofsum"));
+
+ builder.consumer(
+ new MetricsmanagerConfig.Consumer.Builder().
+ name("log").
+ tags("logdefault").
+ removedtags("loadtype"));
+ builder.consumer(
+ new MetricsmanagerConfig.Consumer.Builder().
+ name("yamas").
+ tags("yamasdefault").
+ removedtags("loadtype"));
+ builder.consumer(
+ new MetricsmanagerConfig.Consumer.Builder().
+ name("health"));
+ builder.consumer(
+ new MetricsmanagerConfig.Consumer.Builder().
+ name("fleetcontroller"));
+ builder.consumer(
+ new MetricsmanagerConfig.Consumer.Builder().
+ name("statereporter").
+ addedmetrics("*").
+ removedtags("thread").
+ tags("disk"));
+
+ Map<String, MetricsConsumer> consumers = getRoot().getAdmin().getUserMetricsConsumers();
+ if (consumers != null) {
+ for (Map.Entry<String, MetricsConsumer> e : consumers.entrySet()) {
+ MetricsmanagerConfig.Consumer.Builder b = getMetricBuilder(e.getKey(), builder);
+ for (Metric m : e.getValue().getMetrics().values()) {
+ b.addedmetrics(m.getName());
+ }
+ }
+ }
+
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DocumentSelectionBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DocumentSelectionBuilder.java
new file mode 100644
index 00000000000..b145637b6c5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DocumentSelectionBuilder.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.document.select.DocumentSelector;
+import com.yahoo.document.select.parser.ParseException;
+import com.yahoo.document.select.rule.DocumentNode;
+import com.yahoo.vespa.model.content.DocumentTypeVisitor;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+/**
+ * @author thomasg
+ */
+public class DocumentSelectionBuilder {
+
+ private static class AllowedDocumentTypesChecker extends DocumentTypeVisitor {
+ String allowedType;
+
+ private AllowedDocumentTypesChecker(String allowedType) {
+ this.allowedType = allowedType;
+ }
+
+ @Override
+ public void visit(DocumentNode documentNode) {
+ if (!documentNode.getType().equals(this.allowedType)) {
+ if (this.allowedType == null) {
+ throw new IllegalArgumentException("Document type references are not allowed " +
+ "in global <documents> tag selection attribute (found reference to type '" +
+ documentNode.getType() + "')");
+ } else {
+ throw new IllegalArgumentException("Selection for document type '" +
+ this.allowedType + "' can not contain references to other " +
+ "document types (found reference to type '" + documentNode.getType() + "')");
+ }
+ }
+ }
+ }
+
+ private void validateSelectionExpression(String sel, String allowedType) {
+ DocumentSelector s;
+ try {
+ s = new DocumentSelector(sel);
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Could not parse document routing selection: " + sel, e);
+ }
+ AllowedDocumentTypesChecker checker = new AllowedDocumentTypesChecker(allowedType);
+ s.visit(checker);
+ }
+
+ public String build(ModelElement elem) {
+ StringBuilder sb = new StringBuilder();
+ if (elem != null) {
+ for (ModelElement e : elem.subElements("document")) {
+ if (sb.length() > 0) {
+ sb.append(" OR ");
+ }
+ sb.append('(');
+ String type = e.getStringAttribute("type");
+ sb.append(type);
+ String selection = e.getStringAttribute("selection");
+ if (selection != null) {
+ validateSelectionExpression(selection, type);
+ sb.append(" AND (");
+ sb.append(selection);
+ sb.append(')');
+ }
+ sb.append(')');
+ }
+
+ String globalSelection = elem.getStringAttribute("selection");
+ if (globalSelection != null) {
+ validateSelectionExpression(globalSelection, null);
+ StringBuilder global = new StringBuilder();
+ global.append('(').append(globalSelection).append(") AND (")
+ .append(sb.toString()).append(')');
+ return global.toString();
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilder.java
new file mode 100644
index 00000000000..b43b856b38a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilder.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.content.ContentSearch;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class DomContentSearchBuilder {
+
+ public static ContentSearch build(ModelElement contentXml) {
+ ContentSearch.Builder builder = new ContentSearch.Builder();
+ ModelElement searchElement = contentXml.getChild("search");
+ if (searchElement == null) {
+ return builder.build();
+ }
+ builder.setQueryTimeout(searchElement.childAsDouble("query-timeout"));
+ builder.setVisibilityDelay(searchElement.childAsDouble("visibility-delay"));
+ return builder.build();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomDispatchBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomDispatchBuilder.java
new file mode 100644
index 00000000000..4796c8b1382
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomDispatchBuilder.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+
+import com.yahoo.vespa.model.content.DispatchSpec;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Builder for the dispatch setup for a content cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class DomDispatchBuilder {
+
+ public static DispatchSpec build(ModelElement contentXml) {
+ DispatchSpec.Builder builder = new DispatchSpec.Builder();
+ ModelElement dispatchElement = contentXml.getChild("dispatch");
+ if (dispatchElement == null) {
+ return builder.build();
+ }
+ builder.setNumDispatchGroups(dispatchElement.childAsInteger("num-dispatch-groups"));
+
+ List<ModelElement> groupsElement = dispatchElement.subElements("group");
+ if (groupsElement != null) {
+ builder.setGroups(buildGroups(groupsElement));
+ }
+ return builder.build();
+ }
+
+ private static List<DispatchSpec.Group> buildGroups(List<ModelElement> groupsElement) {
+ List<DispatchSpec.Group> groups = new ArrayList<>();
+ for (ModelElement groupElement : groupsElement) {
+ groups.add(buildGroup(groupElement));
+ }
+ return groups;
+ }
+
+ private static DispatchSpec.Group buildGroup(ModelElement groupElement) {
+ List<ModelElement> nodes = groupElement.subElements("node");
+ DispatchSpec.Group group = new DispatchSpec.Group();
+ for (ModelElement nodeElement : nodes) {
+ group.addNode(buildNode(nodeElement));
+ }
+ return group;
+ }
+
+ private static DispatchSpec.Node buildNode(ModelElement nodeElement) {
+ return new DispatchSpec.Node(nodeElement.getIntegerAttribute("distribution-key"));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java
new file mode 100644
index 00000000000..8c83957eb26
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomResourceLimitsBuilder.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.ResourceLimits;
+
+/**
+ * Builder for resource limits for a content cluster with engine proton.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class DomResourceLimitsBuilder {
+
+ public static ResourceLimits build(ModelElement contentXml) {
+ ResourceLimits.Builder builder = new ResourceLimits.Builder();
+ ModelElement resourceLimits = contentXml.getChild("resource-limits");
+ if (resourceLimits == null) {
+ return builder.build();
+ }
+ builder.setDiskLimit(resourceLimits.childAsDouble("disk"));
+ builder.setMemoryLimit(resourceLimits.childAsDouble("memory"));
+ return builder.build();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilder.java
new file mode 100644
index 00000000000..83602c3003c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilder.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.content.SearchCoverage;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class DomSearchCoverageBuilder {
+
+ public static SearchCoverage build(ModelElement contentXml) {
+ SearchCoverage.Builder builder = new SearchCoverage.Builder();
+ ModelElement searchElement = contentXml.getChild("search");
+ if (searchElement == null) {
+ return builder.build();
+ }
+ ModelElement coverageElement = searchElement.getChild("coverage");
+ if (coverageElement == null) {
+ return builder.build();
+ }
+ builder.setMinimum(coverageElement.childAsDouble("minimum"));
+ builder.setMinWaitAfterCoverageFactor(coverageElement.childAsDouble("min-wait-after-coverage-factor"));
+ builder.setMaxWaitAfterCoverageFactor(coverageElement.childAsDouble("max-wait-after-coverage-factor"));
+ return builder.build();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilder.java
new file mode 100644
index 00000000000..1d2bf938758
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilder.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.content.TuningDispatch;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class DomTuningDispatchBuilder {
+
+ public static TuningDispatch build(ModelElement contentXml) {
+ TuningDispatch.Builder builder = new TuningDispatch.Builder();
+ ModelElement tuningElement = contentXml.getChild("tuning");
+ if (tuningElement == null) {
+ return builder.build();
+ }
+ ModelElement dispatchElement = tuningElement.getChild("dispatch");
+ if (dispatchElement == null) {
+ return builder.build();
+ }
+ builder.setMaxHitsPerPartition(dispatchElement.childAsInteger("max-hits-per-partition"));
+ builder.setDispatchPolicy(dispatchElement.childAsString("dispatch-policy"));
+ builder.setUseLocalNode(dispatchElement.childAsBoolean("use-local-node"));
+ builder.setMinGroupCoverage(dispatchElement.childAsDouble("min-group-coverage"));
+ builder.setMinActiveDocsCoverage(dispatchElement.childAsDouble("min-active-docs-coverage"));
+
+ return builder.build();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/EngineFactoryBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/EngineFactoryBuilder.java
new file mode 100644
index 00000000000..374f970739d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/EngineFactoryBuilder.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.engines.*;
+
+/**
+ * Creates the correct engine factory from XML.
+ */
+public class EngineFactoryBuilder {
+ public PersistenceEngine.PersistenceFactory build(ModelElement clusterElem, ContentCluster c) {
+ ModelElement persistence = clusterElem.getChild("engine");
+ if (persistence != null) {
+ if (c.getSearch().hasIndexedCluster() && persistence.getChild("proton") == null) {
+ throw new IllegalArgumentException("Persistence engine does not allow for indexed search. Please use <proton> as your engine.");
+ }
+
+ ModelElement e;
+ if ((e = persistence.getChild("vds")) != null) {
+ return new VDSEngine.Factory(e);
+ } else if (persistence.getChild("proton") != null) {
+ return new ProtonEngine.Factory(c.getSearch());
+ } else if (persistence.getChild("dummy") != null) {
+ return new com.yahoo.vespa.model.content.engines.DummyPersistence.Factory();
+ } else if (persistence.getChild("rpc") != null) {
+ return new RPCEngine.Factory();
+ }
+ }
+
+ return new ProtonEngine.Factory(c.getSearch());
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java
new file mode 100644
index 00000000000..d80f7029320
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/RedundancyBuilder.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.Redundancy;
+
+/**
+ * Builds redundancy config for a content cluster.
+ */
+public class RedundancyBuilder {
+ Redundancy build(ModelElement clusterXml) {
+ Integer initialRedundancy = 2;
+ Integer finalRedundancy = 3;
+ Integer readyCopies = 2;
+
+ ModelElement redundancyElement = clusterXml.getChild("redundancy");
+ if (redundancyElement != null) {
+ initialRedundancy = redundancyElement.getIntegerAttribute("reply-after");
+ finalRedundancy = (int)redundancyElement.asLong();
+
+ if (initialRedundancy == null) {
+ initialRedundancy = finalRedundancy;
+ } else {
+ if (finalRedundancy < initialRedundancy) {
+ throw new IllegalArgumentException("Final redundancy must be higher than or equal to initial redundancy");
+ }
+ }
+
+ readyCopies = clusterXml.childAsInteger("engine.proton.searchable-copies");
+ if (readyCopies == null) {
+ readyCopies = Math.min(finalRedundancy, 2);
+ }
+ if (readyCopies > finalRedundancy) {
+ throw new IllegalArgumentException("Number of searchable copies can not be higher than final redundancy");
+ }
+ }
+
+ return new Redundancy(initialRedundancy, finalRedundancy, readyCopies);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/SearchDefinitionBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/SearchDefinitionBuilder.java
new file mode 100644
index 00000000000..3f43dbe4491
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/SearchDefinitionBuilder.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.documentmodel.DocumentTypeRepo;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+* Created with IntelliJ IDEA.
+* User: thomasg
+* Date: 9/28/12
+* Time: 1:20 PM
+* To change this template use File | Settings | File Templates.
+*/
+public class SearchDefinitionBuilder {
+ public Map<String, NewDocumentType> build(DocumentTypeRepo repo, ModelElement elem) {
+ Map<String, NewDocumentType> docTypes = new TreeMap<>();
+
+ if (elem != null) {
+ for (ModelElement e : elem.subElements("document")) {
+ String name = e.getStringAttribute("type"); // Schema-guaranteed presence
+ NewDocumentType documentType = repo.getDocumentType(name);
+ if (documentType != null) {
+ docTypes.put(documentType.getName(), documentType);
+ } else {
+ throw new RuntimeException("Document type '" + name + "' not found in application package");
+ }
+ }
+ }
+
+ return docTypes;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/package-info.java
new file mode 100644
index 00000000000..47504d5d0cd
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/cluster/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/engines/DummyPersistence.java b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/DummyPersistence.java
new file mode 100644
index 00000000000..b52b3e0d248
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/DummyPersistence.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.engines;
+
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.StorageGroup;
+import com.yahoo.vespa.model.content.StorageNode;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+public class DummyPersistence extends PersistenceEngine {
+ public DummyPersistence(StorageNode parent) {
+ super(parent, "provider");
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ builder.persistence_provider(new StorServerConfig.Persistence_provider.Builder().type(StorServerConfig.Persistence_provider.Type.Enum.DUMMY));
+ }
+
+ public static class Factory implements PersistenceFactory {
+
+ @Override
+ public PersistenceEngine create(StorageNode storageNode, StorageGroup parentGroup, ModelElement storageNodeElement) {
+ return new DummyPersistence(storageNode);
+ }
+
+ @Override
+ public boolean supportRevert() {
+ return true;
+ }
+
+ @Override
+ public boolean enableMultiLevelSplitting() {
+ return true;
+ }
+
+ @Override
+ public ContentCluster.DistributionMode getDefaultDistributionMode() {
+ return ContentCluster.DistributionMode.LOOSE;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/engines/PersistenceEngine.java b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/PersistenceEngine.java
new file mode 100644
index 00000000000..29ed02ab2b6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/PersistenceEngine.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.engines;
+
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.StorageGroup;
+import com.yahoo.vespa.model.content.StorageNode;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+public abstract class PersistenceEngine extends AbstractConfigProducer implements StorServerConfig.Producer {
+
+ public PersistenceEngine(AbstractConfigProducer parent, String name) {
+ super(parent, name);
+ }
+
+ /**
+ * Creates a config producer for the engines provider at a given node.
+ */
+ public static interface PersistenceFactory {
+ public PersistenceEngine create(StorageNode storageNode, StorageGroup parentGroup, ModelElement storageNodeElement);
+
+ /**
+ * If a write request succeeds on some nodes and fails on others, causing request to
+ * fail to client, the content layer will revert the operation where it succeeded if
+ * reverts are supported. (Typically require backend to keep multiple entries of the
+ * same document identifier persisted at the same time)
+ */
+ public boolean supportRevert();
+
+ /**
+ * Multi level splitting can increase split performance a lot where documents have been
+ * co-localized, for backends where retrieving document identifiers contained in bucket
+ * is cheap. Backends where split is cheaper than fetching document identifiers will
+ * not want to enable multi level splitting.
+ */
+ public boolean enableMultiLevelSplitting();
+
+ public ContentCluster.DistributionMode getDefaultDistributionMode();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonEngine.java b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonEngine.java
new file mode 100644
index 00000000000..595da4da2a0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonEngine.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.engines;
+
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.ContentSearchCluster;
+import com.yahoo.vespa.model.content.StorageGroup;
+import com.yahoo.vespa.model.content.StorageNode;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.search.SearchNode;
+
+/**
+ * Initializes the engines engine on each storage node. May include creating other
+ * nodes.
+ */
+public class ProtonEngine {
+ public static class Factory implements PersistenceEngine.PersistenceFactory {
+ ContentSearchCluster search;
+
+ public Factory(ContentSearchCluster search) {
+ this.search = search;
+ }
+
+ @Override
+ public PersistenceEngine create(StorageNode storageNode, StorageGroup parentGroup, ModelElement storageNodeElement) {
+ SearchNode searchNode = search.addSearchNode(storageNode, parentGroup, storageNodeElement);
+ return new ProtonProvider(storageNode, searchNode);
+ }
+
+ @Override
+ public boolean supportRevert() {
+ return false;
+ }
+
+ @Override
+ public boolean enableMultiLevelSplitting() {
+ return false;
+ }
+
+ @Override
+ public ContentCluster.DistributionMode getDefaultDistributionMode() {
+ return ContentCluster.DistributionMode.LOOSE;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonProvider.java
new file mode 100644
index 00000000000..d665df3d53c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/ProtonProvider.java
@@ -0,0 +1,18 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.engines;
+
+import com.yahoo.vespa.model.content.StorageNode;
+import com.yahoo.vespa.model.search.SearchNode;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: balder
+ * Date: 20.11.12
+ * Time: 20:04
+ * To change this template use File | Settings | File Templates.
+ */
+public class ProtonProvider extends RPCEngine {
+ public ProtonProvider(StorageNode parent, SearchNode searchNode) {
+ super(parent, searchNode);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/engines/RPCEngine.java b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/RPCEngine.java
new file mode 100644
index 00000000000..ec0d7dc35f4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/RPCEngine.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.engines;
+
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.StorageGroup;
+import com.yahoo.vespa.model.content.StorageNode;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.search.SearchNode;
+
+public class RPCEngine extends PersistenceEngine {
+
+ private SearchNode searchNode;
+ public RPCEngine(StorageNode parent) {
+ super(parent, "provider");
+ }
+
+ public RPCEngine(StorageNode parent, SearchNode searchNode) {
+ super(parent, "provider");
+ this.searchNode = searchNode;
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ StorServerConfig.Persistence_provider.Builder provider =
+ new StorServerConfig.Persistence_provider.Builder();
+ provider.type(StorServerConfig.Persistence_provider.Type.Enum.RPC);
+
+ if (searchNode != null) {
+ provider.rpc(new StorServerConfig.Persistence_provider.Rpc.Builder().connectspec("tcp/localhost:" + searchNode.getPersistenceProviderRpcPort()));
+ }
+
+ builder.persistence_provider(provider);
+ }
+
+ public static class Factory implements PersistenceFactory {
+ @Override
+ public PersistenceEngine create(StorageNode storageNode, StorageGroup parentGroup, ModelElement storageNodeElement) {
+ return new RPCEngine(storageNode);
+ }
+
+ @Override
+ public boolean supportRevert() {
+ return false;
+ }
+
+ @Override
+ public boolean enableMultiLevelSplitting() {
+ return false;
+ }
+
+ @Override
+ public ContentCluster.DistributionMode getDefaultDistributionMode() {
+ return ContentCluster.DistributionMode.LOOSE;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/engines/VDSEngine.java b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/VDSEngine.java
new file mode 100644
index 00000000000..5e407938159
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/VDSEngine.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.engines;
+
+import com.yahoo.vespa.config.storage.StorMemfilepersistenceConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.StorageGroup;
+import com.yahoo.vespa.model.content.StorageNode;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+/**
+ * Configuration class to generate config for the memfile engines provider.
+ */
+public class VDSEngine extends PersistenceEngine
+ implements StorMemfilepersistenceConfig.Producer
+{
+ ModelElement tuning;
+
+ public VDSEngine(StorageNode parent, ModelElement vdsConfig) {
+ super(parent, "provider");
+
+ if (vdsConfig != null) {
+ this.tuning = vdsConfig.getChild("tuning");
+ }
+
+ if (parent != null) {
+ parent.useVdsEngine();
+ }
+ }
+
+ @Override
+ public void getConfig(StorMemfilepersistenceConfig.Builder builder) {
+ if (tuning == null) {
+ return;
+ }
+
+ ModelElement diskFullRatio = tuning.getChild("disk-full-ratio");
+ if (diskFullRatio != null) {
+ builder.disk_full_factor(diskFullRatio.asDouble());
+ }
+
+ ModelElement cacheSize = tuning.getChild("cache-size");
+ if (cacheSize != null) {
+ builder.cache_size(cacheSize.asLong());
+ }
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ builder.persistence_provider(
+ new StorServerConfig.Persistence_provider.Builder().type(
+ StorServerConfig.Persistence_provider.Type.Enum.STORAGE)
+ );
+ }
+
+ public static class Factory implements PersistenceFactory {
+ ModelElement vdsConfig;
+
+ public Factory(ModelElement vdsConfig) {
+ this.vdsConfig = vdsConfig;
+ }
+
+ @Override
+ public PersistenceEngine create(StorageNode storageNode, StorageGroup parentGroup, ModelElement storageNodeElement) {
+ return new VDSEngine(storageNode, vdsConfig);
+ }
+
+ @Override
+ public boolean supportRevert() {
+ return true;
+ }
+
+ @Override
+ public boolean enableMultiLevelSplitting() {
+ return true;
+ }
+
+ @Override
+ public ContentCluster.DistributionMode getDefaultDistributionMode() {
+ return ContentCluster.DistributionMode.STRICT;
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/engines/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/package-info.java
new file mode 100644
index 00000000000..28652c82996
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/engines/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.content.engines;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/content/package-info.java
new file mode 100644
index 00000000000..a0af4d2a5fd
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java
new file mode 100644
index 00000000000..6a5f14899ee
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.storagecluster;
+
+import com.yahoo.vespa.config.content.StorFilestorConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.PriorityMapping;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Serves stor-filestor for storage clusters.
+ */
+public class FileStorProducer implements StorFilestorConfig.Producer {
+ public static class Builder {
+ protected FileStorProducer build(ContentCluster parent, ModelElement clusterElem) {
+ return new FileStorProducer(parent, getThreads(clusterElem));
+ }
+
+ private List<StorFilestorConfig.Threads.Builder> getThreads(ModelElement clusterElem) {
+ ModelElement tuning = clusterElem.getChild("tuning");
+ if (tuning == null) {
+ return null;
+ }
+ ModelElement threads = tuning.getChild("persistence-threads");
+ if (threads == null) {
+ return null;
+ }
+
+ List<StorFilestorConfig.Threads.Builder> retVal = new ArrayList<>();
+
+ PriorityMapping mapping = new PriorityMapping(clusterElem);
+
+ for (ModelElement thread : threads.subElements("thread")) {
+ String priorityName = thread.getStringAttribute("lowest-priority");
+ if (priorityName == null) {
+ priorityName = "LOWEST";
+ }
+
+ Integer count = thread.getIntegerAttribute("count");
+ if (count == null) {
+ count = 1;
+ }
+
+ for (int i = 0; i < count; ++i) {
+ retVal.add(new StorFilestorConfig.Threads.Builder().lowestpri(mapping.getPriorityMapping(priorityName)));
+ }
+ }
+
+ return retVal;
+ }
+ }
+
+ private List<StorFilestorConfig.Threads.Builder> threads;
+ private ContentCluster cluster;
+
+ public FileStorProducer(ContentCluster parent, List<StorFilestorConfig.Threads.Builder> threads) {
+ this.threads = threads;
+ this.cluster = parent;
+ }
+
+ @Override
+ public void getConfig(StorFilestorConfig.Builder builder) {
+ if (threads != null) {
+ for (StorFilestorConfig.Threads.Builder t : threads) {
+ builder.threads.add(t);
+ }
+ }
+ builder.enable_multibit_split_optimalization(cluster.getPersistence().enableMultiLevelSplitting());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/IntegrityCheckerProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/IntegrityCheckerProducer.java
new file mode 100644
index 00000000000..69bf29ca307
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/IntegrityCheckerProducer.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.storagecluster;
+
+import com.yahoo.vespa.config.content.core.StorIntegritycheckerConfig;
+import com.yahoo.config.model.ConfigModelUtils;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+/**
+ * Serves stor-integritychecker config for storage clusters.
+ */
+public class IntegrityCheckerProducer implements StorIntegritycheckerConfig.Producer {
+
+ public static class Builder {
+ protected IntegrityCheckerProducer build(ModelElement clusterElem) {
+ ModelElement tuning = clusterElem.getChild("tuning");
+
+ if (tuning == null) {
+ return new IntegrityCheckerProducer();
+ }
+
+ ModelElement maintenance = tuning.getChild("maintenance");
+ if (maintenance == null) {
+ return new IntegrityCheckerProducer();
+ }
+
+ Integer startTime = null;
+ Integer stopTime = null;
+ String weeklyCycle = null;
+
+ String start = maintenance.getStringAttribute("start");
+ if (start != null) {
+ startTime = ConfigModelUtils.getTimeOfDay(start);
+ }
+
+ String stop = maintenance.getStringAttribute("stop");
+ if (stop != null) {
+ stopTime = ConfigModelUtils.getTimeOfDay(stop);
+ }
+
+ String high = maintenance.getStringAttribute("high");
+
+ if (high != null) {
+ int weekday = ConfigModelUtils.getDayOfWeek(high);
+ char[] weeklycycle = "rrrrrrr".toCharArray();
+ weeklycycle[weekday] = 'R';
+ weeklyCycle = String.valueOf(weeklycycle);
+ }
+
+ return new IntegrityCheckerProducer(startTime, stopTime, weeklyCycle);
+ }
+ }
+
+ private Integer startTime;
+ private Integer stopTime;
+ private String weeklyCycle;
+
+ IntegrityCheckerProducer() {
+ }
+
+ IntegrityCheckerProducer(Integer startTime, Integer stopTime, String weeklyCycle) {
+ this.startTime = startTime;
+ this.stopTime = stopTime;
+ this.weeklyCycle = weeklyCycle;
+ }
+
+ @Override
+ public void getConfig(StorIntegritycheckerConfig.Builder builder) {
+ if (startTime != null) {
+ builder.dailycyclestart(startTime);
+ }
+
+ if (stopTime != null) {
+ builder.dailycyclestop(stopTime);
+ }
+
+ if (weeklyCycle != null) {
+ builder.weeklycycle(weeklyCycle);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/PersistenceProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/PersistenceProducer.java
new file mode 100644
index 00000000000..d632329192b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/PersistenceProducer.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.storagecluster;
+
+import com.yahoo.vespa.config.content.PersistenceConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.utils.Duration;
+
+/**
+ * Serves engines config for storage clusters.
+ */
+public class PersistenceProducer implements PersistenceConfig.Producer {
+
+ public static class Builder {
+ public PersistenceProducer build(ModelElement element) {
+ ModelElement persistence = element.getChild("engine");
+ if (persistence == null) {
+ return new PersistenceProducer();
+ }
+
+ return new PersistenceProducer(
+ persistence.childAsBoolean("fail-partition-on-error"),
+ persistence.childAsDuration("recovery-time"),
+ persistence.childAsDuration("revert-time"));
+ }
+ }
+
+ Boolean failOnError;
+ Duration recoveryPeriod;
+ Duration revertTimePeriod;
+
+ public PersistenceProducer() {}
+
+ public PersistenceProducer(Boolean failOnError, Duration recoveryPeriod, Duration revertTimePeriod) {
+ this.failOnError = failOnError;
+ this.recoveryPeriod = recoveryPeriod;
+ this.revertTimePeriod = revertTimePeriod;
+ }
+
+ @Override
+ public void getConfig(PersistenceConfig.Builder builder) {
+ if (failOnError != null) {
+ builder.fail_partition_on_error(failOnError);
+ }
+ if (recoveryPeriod != null) {
+ builder.keep_remove_time_period((int)recoveryPeriod.getSeconds());
+ }
+ if (revertTimePeriod != null) {
+ builder.revert_time_period((int)revertTimePeriod.getSeconds());
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java
new file mode 100644
index 00000000000..e6f767e881c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.storagecluster;
+
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+/**
+ * Serves config for stor-server for storage clusters (clusters of storage nodes).
+ */
+public class StorServerProducer implements StorServerConfig.Producer {
+ public static class Builder {
+ StorServerProducer build(ModelElement element) {
+ ModelElement tuning = element.getChild("tuning");
+
+ if (tuning == null) {
+ return new StorServerProducer(ContentCluster.getClusterName(element), null, null);
+ }
+
+ ModelElement merges = tuning.getChild("merges");
+ if (merges == null) {
+ return new StorServerProducer(ContentCluster.getClusterName(element), null, null);
+ }
+
+ return new StorServerProducer(ContentCluster.getClusterName(element),
+ merges.getIntegerAttribute("max-per-node"),
+ merges.getIntegerAttribute("max-queue-size"));
+ }
+ }
+
+ private String clusterName;
+ private Integer maxMergesPerNode;
+ private Integer queueSize;
+
+ public StorServerProducer(String clusterName, Integer maxMergesPerNode, Integer queueSize) {
+ this.clusterName = clusterName;
+ this.maxMergesPerNode = maxMergesPerNode;
+ this.queueSize = queueSize;
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ builder.root_folder("");
+ builder.is_distributor(false);
+
+ if (clusterName != null) {
+ builder.cluster_name(clusterName);
+ }
+ if (maxMergesPerNode != null) {
+ builder.max_merges_per_node(maxMergesPerNode);
+ }
+ if (queueSize != null) {
+ builder.max_merge_queue_size(queueSize);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorVisitorProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorVisitorProducer.java
new file mode 100644
index 00000000000..b04ef0d15a4
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorVisitorProducer.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.storagecluster;
+
+import com.yahoo.vespa.config.content.core.StorVisitorConfig;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+
+/**
+ * Serves stor-visitor config for storage clusters.
+ */
+public class StorVisitorProducer implements StorVisitorConfig.Producer {
+ public static class Builder {
+ public StorVisitorProducer build(ModelElement element) {
+ ModelElement tuning = element.getChild("tuning");
+ if (tuning == null) {
+ return new StorVisitorProducer();
+ }
+
+ ModelElement visitors = tuning.getChild("visitors");
+ if (visitors == null) {
+ return new StorVisitorProducer();
+ }
+
+ return new StorVisitorProducer(visitors.getIntegerAttribute("thread-count"),
+ visitors.getIntegerAttribute("max-queue-size"),
+ visitors.childAsInteger("max-concurrent.fixed"),
+ visitors.childAsInteger("max-concurrent.variable"));
+ }
+ }
+
+ Integer threadCount;
+ Integer maxQueueSize;
+ Integer maxConcurrentFixed;
+ Integer maxConcurrentVariable;
+
+ public StorVisitorProducer() {}
+
+ StorVisitorProducer(Integer threadCount, Integer maxQueueSize, Integer maxConcurrentFixed, Integer maxConcurrentVariable) {
+ this.threadCount = threadCount;
+ this.maxQueueSize = maxQueueSize;
+ this.maxConcurrentFixed = maxConcurrentFixed;
+ this.maxConcurrentVariable = maxConcurrentVariable;
+ }
+
+ @Override
+ public void getConfig(StorVisitorConfig.Builder builder) {
+ if (threadCount != null) {
+ builder.visitorthreads(threadCount);
+ }
+ if (maxQueueSize != null) {
+ builder.maxvisitorqueuesize(maxQueueSize);
+ }
+ if (maxConcurrentFixed != null) {
+ builder.maxconcurrentvisitors_fixed(maxConcurrentFixed);
+ }
+ if (maxConcurrentVariable != null) {
+ builder.maxconcurrentvisitors_variable(maxConcurrentVariable);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java
new file mode 100644
index 00000000000..6c9cbadd21a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java
@@ -0,0 +1,149 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.storagecluster;
+
+import com.yahoo.vespa.config.content.core.StorIntegritycheckerConfig;
+import com.yahoo.vespa.config.storage.StorMemfilepersistenceConfig;
+import com.yahoo.vespa.config.content.core.StorBucketmoverConfig;
+import com.yahoo.vespa.config.content.core.StorVisitorConfig;
+import com.yahoo.vespa.config.content.StorFilestorConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.config.content.PersistenceConfig;
+import com.yahoo.metrics.MetricsmanagerConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.StorageNode;
+import org.w3c.dom.Element;
+
+/**
+ * Represents configuration that is common to all storage nodes.
+ */
+public class StorageCluster extends AbstractConfigProducer<StorageNode>
+ implements StorServerConfig.Producer,
+ StorBucketmoverConfig.Producer,
+ StorMemfilepersistenceConfig.Producer,
+ StorIntegritycheckerConfig.Producer,
+ StorFilestorConfig.Producer,
+ StorVisitorConfig.Producer,
+ PersistenceConfig.Producer,
+ MetricsmanagerConfig.Producer
+{
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<StorageCluster> {
+ @Override
+ protected StorageCluster doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ ModelElement clusterElem = new ModelElement(producerSpec);
+
+ return new StorageCluster(ancestor,
+ ContentCluster.getClusterName(clusterElem),
+ new FileStorProducer.Builder().build(((ContentCluster)ancestor), clusterElem),
+ new IntegrityCheckerProducer.Builder().build(clusterElem),
+ new StorServerProducer.Builder().build(clusterElem),
+ new StorVisitorProducer.Builder().build(clusterElem),
+ new PersistenceProducer.Builder().build(clusterElem));
+ }
+ }
+
+ private Integer bucketMoverMaxFillAboveAverage = null;
+ private Long cacheSize = null;
+ private Double diskFullPercentage = null;
+
+ private String clusterName;
+ private FileStorProducer fileStorProducer;
+ private IntegrityCheckerProducer integrityCheckerProducer;
+ private StorServerProducer storServerProducer;
+ private StorVisitorProducer storVisitorProducer;
+ private PersistenceProducer persistenceProducer;
+
+ StorageCluster(AbstractConfigProducer parent,
+ String clusterName,
+ FileStorProducer fileStorProducer,
+ IntegrityCheckerProducer integrityCheckerProducer,
+ StorServerProducer storServerProducer,
+ StorVisitorProducer storVisitorProducer,
+ PersistenceProducer persistenceProducer) {
+ super(parent, "storage");
+ this.clusterName = clusterName;
+ this.fileStorProducer = fileStorProducer;
+ this.integrityCheckerProducer = integrityCheckerProducer;
+ this.storServerProducer = storServerProducer;
+ this.storVisitorProducer = storVisitorProducer;
+ this.persistenceProducer = persistenceProducer;
+ }
+
+ @Override
+ public void getConfig(StorBucketmoverConfig.Builder builder) {
+ if (bucketMoverMaxFillAboveAverage != null) {
+ builder.max_target_fill_rate_above_average(bucketMoverMaxFillAboveAverage);
+ }
+ }
+
+ @Override
+ public void getConfig(StorMemfilepersistenceConfig.Builder builder) {
+ if (cacheSize != null) {
+ builder.cache_size(cacheSize);
+ }
+
+ if (diskFullPercentage != null) {
+ builder.disk_full_factor(diskFullPercentage / 100.0);
+ builder.disk_full_factor_move(diskFullPercentage / 100.0 * 0.9);
+ }
+ }
+
+ @Override
+ public void getConfig(MetricsmanagerConfig.Builder builder) {
+ ContentCluster.getMetricBuilder("fleetcontroller", builder).
+ addedmetrics("vds.filestor.*.allthreads.put.sum").
+ addedmetrics("vds.filestor.*.allthreads.get.sum").
+ addedmetrics("vds.filestor.*.allthreads.multi.sum").
+ addedmetrics("vds.filestor.*.allthreads.update.sum").
+ addedmetrics("vds.filestor.*.allthreads.remove.sum").
+ addedmetrics("vds.filestor.*.allthreads.operations").
+ addedmetrics("vds.datastored.alldisks.docs").
+ addedmetrics("vds.datastored.alldisks.bytes").
+ addedmetrics("vds.datastored.alldisks.buckets");
+
+ ContentCluster.getMetricBuilder("log", builder).
+ addedmetrics("vds.filestor.alldisks.allthreads.put.sum").
+ addedmetrics("vds.filestor.alldisks.allthreads.get.sum").
+ addedmetrics("vds.filestor.alldisks.allthreads.remove.sum").
+ addedmetrics("vds.filestor.alldisks.allthreads.update.sum").
+ addedmetrics("vds.datastored.alldisks.docs").
+ addedmetrics("vds.datastored.alldisks.bytes").
+ addedmetrics("vds.filestor.alldisks.queuesize").
+ addedmetrics("vds.filestor.alldisks.averagequeuewait.sum").
+ addedmetrics("vds.visitor.cv_queuewaittime").
+ addedmetrics("vds.visitor.allthreads.averagequeuewait").
+ addedmetrics("vds.visitor.allthreads.averagevisitorlifetime").
+ addedmetrics("vds.visitor.allthreads.created.sum");
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ @Override
+ public void getConfig(StorIntegritycheckerConfig.Builder builder) {
+ integrityCheckerProducer.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(StorServerConfig.Builder builder) {
+ storServerProducer.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(StorVisitorConfig.Builder builder) {
+ storVisitorProducer.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(PersistenceConfig.Builder builder) {
+ persistenceProducer.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(StorFilestorConfig.Builder builder) {
+ fileStorProducer.getConfig(builder);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/package-info.java
new file mode 100644
index 00000000000..6b3ca6a06b7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.content.storagecluster;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributionConfigProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributionConfigProducer.java
new file mode 100644
index 00000000000..c3fcdddb273
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributionConfigProducer.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.filedistribution;
+
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.admin.FileDistributionOptions;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ */
+public class FileDistributionConfigProducer extends AbstractConfigProducer {
+ private final Map<Host, FileDistributorService> fileDistributorServices = new IdentityHashMap<>();
+ private final FileDistributor fileDistributor;
+ private final FileDistributionOptions options;
+
+ private FileDistributionConfigProducer(AbstractConfigProducer parent, FileDistributor fileDistributor, FileDistributionOptions options) {
+ super(parent, "filedistribution");
+ this.fileDistributor = fileDistributor;
+ this.options = options;
+ }
+
+ public String getFileDistributionServiceConfigId(Host host) {
+ FileDistributorService service = fileDistributorServices.get(host);
+ if (service == null) {
+ throw new IllegalStateException("No file distribution service for host " + host);
+ }
+ return service.getConfigId();
+ }
+
+ public FileDistributor getFileDistributor() {
+ return fileDistributor;
+ }
+
+ public FileDistributionOptions getOptions() {
+ return options;
+ }
+
+ public void addFileDistributionService(Host host, FileDistributorService fds) {
+ fileDistributorServices.put(host, fds);
+ }
+
+ public static class Builder {
+
+ private final FileDistributionOptions options;
+
+ public Builder(FileDistributionOptions fileDistributionOptions) {
+ this.options = fileDistributionOptions;
+ }
+
+ public FileDistributionConfigProducer build(AbstractConfigProducer ancestor, FileRegistry fileRegistry) {
+ FileDistributor fileDistributor = new FileDistributor(fileRegistry);
+ return new FileDistributionConfigProducer(ancestor, fileDistributor, options);
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributor.java b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributor.java
new file mode 100644
index 00000000000..4dc24618a61
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributor.java
@@ -0,0 +1,120 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.vespa.model.Host;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.asList;
+
+
+/**
+ * Responsible for directing distribution of files to hosts.
+ *
+ * @author tonytv
+ */
+public class FileDistributor {
+
+ private final FileRegistry fileRegistry;
+
+ /** A map from files to the hosts to which that file should be distributed */
+ private final Map<FileReference, Set<Host>> filesToHosts = new LinkedHashMap<>();
+
+ /**
+ * Adds the given file to the associated application packages' registry of file and marks the file
+ * for distribution to the given hosts.
+ * <b>Note: This class receives ownership of the given collection.</b>
+ *
+ * @return the reference to the file, created by the application package
+ */
+ public FileReference sendFileToHosts(String relativePath, Collection<Host> hosts) {
+ FileReference reference = fileRegistry.addFile(relativePath);
+ addToFilesToDistribute(reference, hosts);
+
+ return reference;
+ }
+
+ /** Same as sendFileToHost(relativePath,Collections.singletonList(host) */
+ public FileReference sendFileToHost(String relativePath, Host host) {
+ return sendFileToHosts(relativePath, Arrays.asList(host));
+ }
+
+ private void addToFilesToDistribute(FileReference reference, Collection<Host> hosts) {
+ Set<Host> oldHosts = getHosts(reference);
+ oldHosts.addAll(hosts);
+ }
+
+ private Set<Host> getHosts(FileReference reference) {
+ Set<Host> hosts = filesToHosts.get(reference);
+ if (hosts == null) {
+ hosts = new HashSet<>();
+ filesToHosts.put(reference, hosts);
+ }
+ return hosts;
+ }
+
+ public FileDistributor(FileRegistry fileRegistry) {
+ this.fileRegistry = fileRegistry;
+ }
+
+ /** Returns the files which has been marked for distribution to the given host */
+ public Set<FileReference> filesToSendToHost(Host host) {
+ Set<FileReference> files = new HashSet<>();
+
+ for (Map.Entry<FileReference,Set<Host>> e : filesToHosts.entrySet()) {
+ if (e.getValue().contains(host)) {
+ files.add(e.getKey());
+ }
+ }
+ return files;
+ }
+
+ public Set<Host> getTargetHosts() {
+ Set<Host> hosts = new HashSet<>();
+ for (Set<Host> hostSubset: filesToHosts.values())
+ hosts.addAll(hostSubset);
+ return hosts;
+ }
+
+ public Set<String> getTargetHostnames() {
+ return getTargetHosts().stream().map(Host::getHostName).collect(Collectors.toSet());
+ }
+
+ /** Returns the host which is the source of the files */
+ public String fileSourceHost() {
+ return fileRegistry.fileSourceHost();
+ }
+
+ public Set<FileReference> allFilesToSend() {
+ return filesToHosts.keySet();
+ }
+
+ // should only be called during deploy
+ public void sendDeployedFiles(FileDistribution dbHandler) {
+ String fileSourceHost = fileSourceHost();
+ for (Host host : getTargetHosts()) {
+ if ( ! host.getHostName().equals(fileSourceHost)) {
+ dbHandler.sendDeployedFiles(host.getHostName(), filesToSendToHost(host));
+ }
+ }
+ dbHandler.sendDeployedFiles(fileSourceHost, allFilesToSend());
+ dbHandler.limitSendingOfDeployedFilesTo(union(getTargetHostnames(), fileSourceHost));
+ dbHandler.removeDeploymentsThatHaveDifferentApplicationId(getTargetHostnames());
+ }
+
+ // should only be called during deploy, and only once, since it leads to file distributor
+ // rescanning all files, which is very expensive ATM (April 2016)
+ public void reloadDeployFileDistributor(FileDistribution dbHandler) {
+ dbHandler.reloadDeployFileDistributor();
+ }
+
+ private Set<String> union(Set<String> hosts, String... additionalHosts) {
+ Set<String> result = new HashSet<>(hosts);
+ result.addAll(asList(additionalHosts));
+ return result;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributorService.java b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributorService.java
new file mode 100644
index 00000000000..bfa32c003e0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/FileDistributorService.java
@@ -0,0 +1,98 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.cloud.config.filedistribution.*;
+import com.yahoo.config.model.api.FileDistribution;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.admin.FileDistributionOptions;
+
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * @author tonytv
+ */
+public class FileDistributorService extends AbstractService implements
+ FiledistributorConfig.Producer,
+ FiledistributorrpcConfig.Producer,
+ FilereferencesConfig.Producer {
+ private final static int BASEPORT = 19092;
+
+ private final FileDistributor fileDistributor;
+ private final FileDistributionOptions fileDistributionOptions;
+ private final boolean sendAllFiles;
+
+ private Collection<FileReference> getFileReferences() {
+ if (sendAllFiles) {
+ return fileDistributor.allFilesToSend();
+ } else {
+ return fileDistributor.filesToSendToHost(getHost());
+ }
+ }
+
+ public FileDistributorService(AbstractConfigProducer parent,
+ String name,
+ FileDistributor fileDistributor,
+ FileDistributionOptions fileDistributionOptions,
+ boolean sendAllFiles) {
+ super(parent, name);
+ portsMeta.on(0).tag("rpc");
+ portsMeta.on(1).tag("torrent");
+ portsMeta.on(2).tag("http").tag("state");
+ setProp("clustertype", "filedistribution");
+ setProp("clustername", "admin");
+
+ this.fileDistributor = fileDistributor;
+ this.fileDistributionOptions = fileDistributionOptions;
+ this.sendAllFiles = sendAllFiles;
+ monitorService();
+ }
+
+ @Override
+ public String getStartupCommand() {
+ return "exec $ROOT/sbin/filedistributor"
+ + " --configid " + getConfigId();
+ }
+
+ @Override
+ public boolean getAutostartFlag() {
+ return true;
+ }
+
+ @Override
+ public boolean getAutorestartFlag() {
+ return true;
+ }
+
+ public int getPortCount() {
+ return 3;
+ }
+
+ @Override
+ public int getWantedPort() {
+ return BASEPORT;
+ }
+
+ @Override
+ public void getConfig(FiledistributorConfig.Builder builder) {
+ fileDistributionOptions.getConfig(builder);
+ builder.torrentport(getRelativePort(1));
+ builder.stateport(getRelativePort(2));
+ builder.hostname(getHostName());
+ builder.filedbpath(FileDistribution.getDefaultFileDBPath().toString());
+ }
+
+ @Override
+ public void getConfig(FiledistributorrpcConfig.Builder builder) {
+ builder.connectionspec("tcp/" + getHostName() + ":" + getRelativePort(0));
+ }
+
+ @Override
+ public void getConfig(FilereferencesConfig.Builder builder) {
+ for (FileReference reference : getFileReferences()) {
+ builder.filereferences(reference.value());
+ }
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/package-info.java
new file mode 100644
index 00000000000..bc53f9222de
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/filedistribution/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.filedistribution;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesBuilder.java
new file mode 100644
index 00000000000..7bbe9d14bc5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesBuilder.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic;
+
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.vespa.model.generic.builder.DomServiceClusterBuilder;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class GenericServicesBuilder extends ConfigModelBuilder<GenericServicesModel> {
+
+ public GenericServicesBuilder() {
+ super(GenericServicesModel.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return Arrays.asList(ConfigModelId.fromName("service"));
+ }
+
+ @Override
+ public void doBuild(GenericServicesModel model, Element spec, ConfigModelContext modelContext) {
+ String name = spec.getAttribute("name");
+ model.addCluster(new DomServiceClusterBuilder(name).build(modelContext.getParentProducer(), spec));
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesModel.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesModel.java
new file mode 100644
index 00000000000..c6b2e305658
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/GenericServicesModel.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class GenericServicesModel extends ConfigModel {
+ private final List<ServiceCluster> clusters = new ArrayList<>();
+ public GenericServicesModel(ConfigModelContext modelContext) {
+ super(modelContext);
+ }
+
+ public void addCluster(ServiceCluster cluster) {
+ clusters.add(cluster);
+ }
+
+ public List<ServiceCluster> serviceClusters() {
+ return clusters;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomModuleBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomModuleBuilder.java
new file mode 100644
index 00000000000..f4470f0dc9b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomModuleBuilder.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic.builder;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.generic.service.Module;
+import org.w3c.dom.Element;
+
+/**
+ * Produces sub services for generic services.
+ */
+public class DomModuleBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Module> {
+ private final String name;
+
+ public DomModuleBuilder(String name) {
+ this.name = name;
+ }
+
+ private void addChildren(Module s, Element subServiceSpec) {
+ for (Element nodeSpec : XML.getChildren(subServiceSpec, "module")) {
+ new DomModuleBuilder(nodeSpec.getAttribute("name")).build(s, nodeSpec);
+ }
+ }
+
+ @Override
+ protected Module doBuild(AbstractConfigProducer ancestor, Element subServiceSpec) {
+ Module s = new Module(ancestor, name);
+ addChildren(s, subServiceSpec);
+ return s;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceBuilder.java
new file mode 100644
index 00000000000..3cfd85c956b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceBuilder.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic.builder;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.generic.service.Service;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+import org.w3c.dom.Element;
+
+/**
+* @author lulf
+* @since 5.1
+*/
+public class DomServiceBuilder extends VespaDomBuilder.DomConfigProducerBuilder<Service> {
+ private final int i;
+
+ public DomServiceBuilder(int i) {
+ this.i = i;
+ }
+
+ @Override
+ protected com.yahoo.vespa.model.generic.service.Service doBuild(AbstractConfigProducer parent,
+ Element serviceSpec) {
+ ServiceCluster sc = (ServiceCluster) parent;
+ com.yahoo.vespa.model.generic.service.Service service = new com.yahoo.vespa.model.generic.service.Service(sc, i + "");
+ for (Element subServiceSpec : XML.getChildren(serviceSpec, "module")) {
+ new DomModuleBuilder(subServiceSpec.getAttribute("name")).build(service, subServiceSpec);
+ }
+ return service;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceClusterBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceClusterBuilder.java
new file mode 100644
index 00000000000..d537afdd30e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/DomServiceClusterBuilder.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic.builder;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+import org.w3c.dom.Element;
+import java.util.Map;
+
+/**
+* @author lulf
+* @since 5.1
+*/
+public class DomServiceClusterBuilder extends VespaDomBuilder.DomConfigProducerBuilder<ServiceCluster> {
+
+ private String name;
+
+ public DomServiceClusterBuilder(String name) {
+ this.name = name;
+ }
+
+ @Override
+ protected ServiceCluster doBuild(AbstractConfigProducer ancestor, Element spec) {
+ ServiceCluster cluster = new ServiceCluster(ancestor, name, spec.getAttribute("command"));
+ int nodeIndex = 0;
+ for (Element nodeSpec : XML.getChildren(spec, "node")) {
+ com.yahoo.vespa.model.generic.service.Service service = new DomServiceBuilder(nodeIndex).build(cluster, nodeSpec);
+
+ // TODO: Currently creates the config for each service. Should instead build module tree first
+ // and store them in ServiceCluster. Then have some way of referencing them from each service.
+ for (Element subServiceSpec : XML.getChildren(spec, "module")) {
+ String subServiceName = subServiceSpec.getAttribute("name");
+ Map<String, AbstractConfigProducer<?>> map = service.getChildren();
+ // Add only non-conflicting modules. Does not merge unspecified configs that are specified in root though.
+ if (!map.containsKey(subServiceName))
+ new DomModuleBuilder(subServiceName).build(service, subServiceSpec);
+ }
+ nodeIndex++;
+ }
+ return cluster;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/package-info.java
new file mode 100644
index 00000000000..77679e00d08
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/builder/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.generic.builder;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/package-info.java
new file mode 100644
index 00000000000..6922e6138f5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.generic;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/service/Module.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/Module.java
new file mode 100644
index 00000000000..82190e3344e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/Module.java
@@ -0,0 +1,18 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic.service;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+/**
+ * A simple sub service that is essentially just to have a node with a nice name
+ * in the tree. Could might as well have used an AbstractConfigProducer as well,
+ * but that makes the code very confusing to read.
+ *
+ * @author lulf
+ */
+public class Module extends AbstractConfigProducer {
+
+ public Module(AbstractConfigProducer parent, String subId) {
+ super(parent, subId);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/service/Service.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/Service.java
new file mode 100644
index 00000000000..77ebde12634
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/Service.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic.service;
+
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.HostResource;
+
+/**
+ * An application specific generic service
+ * @author vegardh
+ *
+ */
+public class Service extends AbstractService {
+ private static final long serialVersionUID = 1L;
+
+ public Service(ServiceCluster parent, String id) {
+ super(parent, id);
+ setProp("clustertype", parent.getName());
+ setProp("clustername", parent.getName());
+ }
+
+ @Override
+ public int getPortCount() {
+ return 0;
+ }
+
+ @Override
+ public String getStartupCommand() {
+ return ((ServiceCluster) getParent()).getCommand();
+ }
+
+ private String getClusterName() {
+ return ((ServiceCluster) getParent()).getName();
+ }
+
+ /**
+ * Different services are represented using same class here, so we must take service name into account too
+ *
+ * @param host a host
+ * @return the index of the host
+ */
+ protected int getIndex(HostResource host) {
+ int i = 0;
+ for (com.yahoo.vespa.model.Service s : host.getServices()) {
+ if (!s.getClass().equals(getClass())) continue;
+ Service other = (Service)s;
+ if (s!=this && other.getClusterName().equals(getClusterName())) {
+ i++;
+ }
+ }
+ return i + 1;
+ }
+
+ @Override
+ public String getServiceType() {
+ return getClusterName();
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/service/ServiceCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/ServiceCluster.java
new file mode 100644
index 00000000000..b81841442de
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/ServiceCluster.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic.service;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.HostSystem;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * A cluster of nodes running one application specific generic service. These are defined on the top level in the Vespa config
+ * @author vegardh
+ *
+ */
+public class ServiceCluster extends AbstractConfigProducer {
+
+ private static final long serialVersionUID = 1L;
+ private String command;
+ private String name;
+ private HostSystem hostSystem; // A generic cluster can resolve hosts for its nodes
+
+ public ServiceCluster(AbstractConfigProducer parent, String name, String command) {
+ super(parent, name);
+ this.command=command;
+ this.name=name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ String getCommand() {
+ return command;
+ }
+
+ public Collection<Service> services() {
+ Collection<Service> ret = new ArrayList<>();
+ for (Object child : getChildren().values()) {
+ if (child instanceof Service) ret.add((Service) child);
+ }
+ return ret;
+ }
+
+ @Override
+ public HostSystem getHostSystem() {
+ if (hostSystem!=null) return hostSystem;
+ return super.getHostSystem();
+ }
+
+ /**
+ * Sets the host system for this.
+ * @param hostSystem a {@link com.yahoo.vespa.model.HostSystem}
+ */
+ public void setHostSystem(HostSystem hostSystem) {
+ this.hostSystem = hostSystem;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/generic/service/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/package-info.java
new file mode 100644
index 00000000000..49683ab3d97
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/generic/service/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.generic.service;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/package-info.java
new file mode 100644
index 00000000000..4d185d0bb19
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/package-info.java
@@ -0,0 +1,188 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ Provides the classes for the Vespa config model framework.
+
+ <p>The {@link com.yahoo.vespa.model.VespaModel VespaModel} class
+ is the natural starting point. It reads the user-defined
+ application specification, handles <a
+ href="#plugin_loading">plugin loading</a> and currently
+ instantiates one {@link com.yahoo.config.model.ApplicationConfigProducerRoot Vespa}
+ object. VespaModel is the root node in a tree of {@link
+com.yahoo.config.model.producer.AbstractConfigProducer
+ AbstractConfigProducers} that is built from the structure of the
+ user's specification. In a future version, the VespaModel can
+ contain multiple Vespa instances, each built from a separate user
+ specification (currently called 'services.xml').
+ </p>
+
+ <p>Each AbstractConfigProducer in the tree represents an actual
+ service or another logical unit in the Vespa system. An example of
+ a logical unit is a cluster that holds a set of services. Each
+ child class of {@link com.yahoo.config.model.producer.AbstractConfigProducer
+ AbstractConfigProducer} can contain hard-wired config that should
+ be delivered to the Vespa unit it represents, and its children. It
+ can also keep track of the status of the unit.
+ </p>
+
+ <p>A service that runs on a hardware host is always represented by
+ an {@link com.yahoo.vespa.model.AbstractService AbstractService}
+ object, containing the command that will be used to start the
+ service, which host it is running on, and the ports that it
+ uses. Each hardware host in the Vespa system is represented by a
+ {@link com.yahoo.vespa.model.Host Host} object, and the set of
+ hosts is handled by the {@link com.yahoo.vespa.model.HostSystem
+ HostSystem}. Each Host is responsible for avoiding port collisions
+ between services, see <a href="#port_allocation">port
+ allocation</a>.
+ </p>
+
+ <h3>Config Generation</h3>
+
+ <p>The method {@link
+ com.yahoo.vespa.model.VespaModel#getConfig(com.yahoo.config.ConfigInstance.Builder, String)
+ VespaModel.getConfig} looks up the ConfigProducer with the config
+ ID that config is requested for. The composition of the actual
+ config starts from the root node of the ConfigProducer tree, which
+ is always an instance of the {@link com.yahoo.config.model.ApplicationConfigProducerRoot
+ Vespa} class, and traverses each level of the tree back down to
+ the ConfigProducer that got the first call from the root node.
+ This is handled in such a way
+ that config from the root node gets the lowest priority, and the
+ ConfigProducer itself has the highest priority when the same
+ parameter is given different values in the path down the tree.</p>
+
+ <p>User defined configuration can be embedded in the service setup
+ file in the application specification. Currently this is done by adding
+ &lt;config&gt; tags at the desired position in the file named
+ 'services.xml', where each position corresponds to a
+ ConfigProducer. These config values have a higher priority than
+ the default config returned from the same method. However, it can be overridden by the config
+ from a ConfigProducer at a lower level, both by its getConfig
+ method and by user defined config.
+ </p>
+
+ <h4>Example:</h4>
+ <p>
+ Say we have a config named 'sample' with an integer parameter
+ named 'v'. If the VespaModel root node's {@link
+ com.yahoo.vespa.model.VespaModel#getConfig(com.yahoo.config.ConfigInstance.Builder,String)
+ getConfig(builder, configid)} method returns a hardcoded value of
+ 'v=2' for that parameter, this becomes the default value for all
+ ConfigProducers when asking for the 'sample' config. Now, let's
+ assume that we need the 'sample' config for a ConfigProducer of
+ class 'Grandchild', which has a configId
+ 'grandchild_0'. grandchild_0's parent in the ConfigProducer tree
+ is a ConfigProducer of class 'Child' and configId 'child_0' which
+ is a direct child of the Vespa root node:
+ </p>
+
+ <p>The initial step when retrieving a config is always a call to
+ {@link
+ com.yahoo.vespa.model.VespaModel#getConfig(com.yahoo.config.ConfigInstance.Builder,String)
+ VespaModel.getConfig(builder, configId}. Here, the call
+ could look like this:
+ VespaModel.getConfig(builder, &quot;grandchild_0&quot;).
+ This triggers a call to the {@link
+ com.yahoo.config.model.producer.AbstractConfigProducer#cascadeConfig(com.yahoo.config.ConfigInstance.Builder)}) AbstractConfigProducer.cascadeConfig} method for
+ grandchild_0 which calls the same method in child_0, and finally
+ in the VespaModel root node, where the {@link
+ com.yahoo.vespa.model.VespaModel#getConfig(com.yahoo.config.ConfigInstance.Builder,String)
+ getConfig (name, namespace)} method returns the value 'v=2' as
+ previously mentioned. This value might be overridden on the
+ traversal back down in the tree, first in child_0, which could
+ return the value 'v=1'. Now, if the user specification for child_0
+ contains the value 'v=0', this overrides the previous values. The
+ same happens for grandchild_0: if there is a value returned from
+ the getConfig() method, this overrides the value from child_0, and
+ if there is a value from the user specification for grandchild_0,
+ that will always become the final result.
+ </p>
+
+
+ <h3><a name="plugin_loading">Plugin Loading</a></h3>
+
+ <p>Each highest-level node in the setup file from the user's
+ application specification corresponds to a {@link
+com.yahoo.config.model.builder.xml.ConfigModelBuilder ConfigModelBuilder}. The
+ builders are loaded when the system is started. Each builder produce
+ a {@link com.yahoo.config.model.ConfigModel ConfigModel}. The model can depend
+ on other models by having them injected in its constructor. This ensures
+ that the builders are invoked in the correct order as well.
+ In its build method, the builder is responsible for building all its
+ ConfigProducers, and linking them to the parent ConfigProducer
+ given as input argument.
+ </p>
+
+ <p>The built models are given to other models that depends on it.
+ </p>
+
+ <h4>Important notes for plugin developers:</h4>
+ <ul>
+
+ <li>The constructors of all child classes of {@link
+com.yahoo.config.model.producer.AbstractConfigProducer
+ AbstractConfigProducer} should throw a new 'RuntimeException' upon
+ errors in xml or other initialization problems. This allows the
+ exception to be nested upwards, adding valuable information from
+ each level in the ConfigProducer tree to the error message output
+ to the user. The exception should contain detailed information
+ about the error that occurred.
+ </li>
+
+ <li>The plugins are not allowed to put any constraints on the
+ contents of the hosts specification file (currently named
+ 'hosts.xml'), such as demanding special hostnames for
+ different service types. This file belongs solely to the
+ vespamodel framework.
+ </li>
+ </ul>
+
+
+ <h3><a name="port_allocation">Port Allocation</a></h3>
+
+ <p>Each {@link com.yahoo.vespa.model.Host Host} has an available
+ dynamic port range running from {@link
+ com.yahoo.vespa.model.HostResource#BASE_PORT BASE_PORT} (currently 19100)
+ with {@link com.yahoo.vespa.model.HostResource#MAX_PORTS MAX_PORTS}
+ (currently 799) ports upwards. When an instance of a subclass of
+ {@link com.yahoo.vespa.model.AbstractService AbstractService} is
+ assigned to a host, it is given the lowest available base port in
+ this range. The service owns a continuous port range of {@link
+ com.yahoo.vespa.model.Service#getPortCount Service.getPortCount}
+ ports upwards from the base port.
+ </p>
+
+ <p>The base port for a specific service instance on a host is
+ decided by {@link
+ com.yahoo.vespa.model.AbstractService #getInstanceWantedPort
+ AbstractService.getInstanceWantedPort}. The most important aspects
+ are described below:
+ </p>
+
+ <p>It is not possible to reserve a certain port inside the dynamic
+ range, but a service can specify that it wants a base port outside
+ the range by overriding the {@link
+ com.yahoo.vespa.model.Service #getWantedPort Service.getWantedPort}
+ method. If the service type is required to run with the specified
+ base port, it must also override the {@link
+ com.yahoo.vespa.model.Service #requiresWantedPort
+ Service.requiresWantedPort}. The user specified port number
+ returned from {@link com.yahoo.vespa.model.Service #getWantedPort
+ getWantedPort} applies to the first instance of that specific
+ subclass on each host, and the next instance on the same host
+ <em>must</em> have its baseport specified by the 'baseport' attribute
+ in 'services.xml'
+ </p>
+
+ <p>The user-defined application specification can also give a
+ required base port for each individual service. Currently this is
+ done by adding a 'baseport' attribute to the service's tag in the
+ file named 'hosts.xml'. If the port is not available, an
+ exception will be thrown.
+ </p>
+
+*/
+@ExportPackage
+package com.yahoo.vespa.model;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/routing/DocumentProtocol.java b/config-model/src/main/java/com/yahoo/vespa/model/routing/DocumentProtocol.java
new file mode 100644
index 00000000000..344c5e16d29
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/routing/DocumentProtocol.java
@@ -0,0 +1,385 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.routing;
+
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.documentapi.messagebus.protocol.DocumentrouteselectorpolicyConfig;
+import com.yahoo.document.select.DocumentSelector;
+import com.yahoo.messagebus.routing.ApplicationSpec;
+import com.yahoo.messagebus.routing.HopSpec;
+import com.yahoo.messagebus.routing.RouteSpec;
+import com.yahoo.messagebus.routing.RoutingTableSpec;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * This class is responsible for generating all hops and routes for the Document protocol running on message bus. All
+ * the code within could really be part of {@link Routing}, but it has been partitioned out to allow better readability
+ * and also more easily maintainable as the number of protocols increase.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class DocumentProtocol implements Protocol, DocumentrouteselectorpolicyConfig.Producer {
+
+ private static final String NAME = "document";
+ private ApplicationSpec application;
+ private RoutingTableSpec routingTable;
+ ConfigModelRepo repo;
+
+ public static String getIndexedRouteName(String configId) {
+ return configId + "-index";
+ }
+
+ public static String getDirectRouteName(String configId) {
+ return configId + "-direct";
+ }
+
+ /**
+ * Constructs a new document protocol based on the content of the given plugins.
+ *
+ * @param plugins The plugins to reflect on.
+ */
+ public DocumentProtocol(ConfigModelRepo plugins) {
+ application = createApplicationSpec(plugins);
+ routingTable = createRoutingTable(plugins);
+ this.repo = plugins;
+ }
+
+ /**
+ * Creates a service index based on the plugins loaded. This means to fill the index with all services known by this
+ * protocol by traversing the plugins.
+ *
+ * @param plugins All initialized plugins of the Vespa model.
+ * @return The index of all known services.
+ */
+ private static ApplicationSpec createApplicationSpec(ConfigModelRepo plugins) {
+ ApplicationSpec ret = new ApplicationSpec();
+
+ for (ContentCluster cluster : Content.getContentClusters(plugins)) {
+ for (com.yahoo.vespa.model.content.Distributor node : cluster.getDistributorNodes().getChildren().values()) {
+ ret.addService(NAME, node.getConfigId() + "/default");
+ }
+ }
+
+ for (ContainerCluster containerCluster: ContainerModel.containerClusters(plugins)) {
+ ContainerDocproc containerDocproc = containerCluster.getDocproc();
+ if (containerDocproc != null) {
+ createDocprocChainSpec(ret,
+ containerDocproc.getChains().allChains().allComponents(),
+ containerCluster.getContainers());
+ }
+ }
+
+ return ret;
+ }
+
+ private static void createDocprocChainSpec(ApplicationSpec spec,
+ List<DocprocChain> docprocChains,
+ List<Container> containerNodes) {
+ for (DocprocChain chain: docprocChains) {
+ for (Container node: containerNodes)
+ spec.addService(NAME, node.getConfigId() + "/chain." + chain.getComponentId().stringValue());
+ }
+ }
+
+ @Override
+ public void getConfig(DocumentrouteselectorpolicyConfig.Builder builder) {
+ for (ContentCluster cluster : Content.getContentClusters(repo)) {
+ addRoute(cluster.getConfigId(), cluster.getRoutingSelector(), builder);
+ }
+ }
+
+ private static void addRoute(String clusterConfigId, String selector, DocumentrouteselectorpolicyConfig.Builder builder) {
+ try {
+ new DocumentSelector(selector);
+ } catch (com.yahoo.document.select.parser.ParseException e) {
+ throw new IllegalArgumentException("Failed to parse selector '" + selector + "' for route '" + clusterConfigId +
+ "' in policy 'DocumentRouteSelector'.");
+ }
+ DocumentrouteselectorpolicyConfig.Route.Builder routeBuilder = new DocumentrouteselectorpolicyConfig.Route.Builder();
+ routeBuilder.name(clusterConfigId);
+ routeBuilder.selector(selector);
+ builder.route(routeBuilder);
+ }
+
+ /**
+ * This function extrapolates any routes for the document protocol that it can from the vespa model.
+ *
+ * @param plugins All initialized plugins of the vespa model.
+ * @return Routing table for the document protocol.
+ */
+ private static RoutingTableSpec createRoutingTable(ConfigModelRepo plugins) {
+ // Build simple hops and routes.
+ List<ContentCluster> content = Content.getContentClusters(plugins);
+ Collection<ContainerCluster> containerClusters = ContainerModel.containerClusters(plugins);
+
+ RoutingTableSpec table = new RoutingTableSpec(NAME);
+ addContainerClusterDocprocHops(containerClusters, table);
+ addContentRouting(content, table);
+
+ // Build the indexing hop if it is possible to derive.
+ addIndexingHop(content, table);
+
+ // Build the default route if is is possible to derive.
+ addDefaultRoute(content, containerClusters, table);
+
+ // Return the complete routing table.
+ simplifyRouteNames(table);
+ return table;
+ }
+
+ private static void addContainerClusterDocprocHops(Collection<ContainerCluster> containerClusters,
+ RoutingTableSpec table) {
+
+ for (ContainerCluster cluster: containerClusters) {
+ ContainerDocproc docproc = cluster.getDocproc();
+
+ if (docproc != null) {
+ String policy = policy(docproc);
+
+ for (DocprocChain chain : docproc.getChains().allChains().allComponents()) {
+ addChainHop(table, cluster.getConfigId(), policy, chain);
+ }
+ }
+ }
+ }
+
+ private static void addChainHop(RoutingTableSpec table, String configId, String policy, DocprocChain chain) {
+ final String selector;
+ if (policy != null) {
+ selector = configId + "/" + policy + "/" + chain.getSessionName();
+ } else {
+ selector = "[LoadBalancer:cluster=" + configId +
+ ";session=" + chain.getSessionName() + "]";
+ }
+ table.addHop(new HopSpec(chain.getServiceName(), selector));
+ }
+
+ private static String policy(ContainerDocproc docproc) {
+ if (docproc.getNumNodesPerClient() > 0) {
+ return "[SubsetService:" + docproc.getNumNodesPerClient() + "]";
+ } else if (docproc.isPreferLocalNode()) {
+ return "[LocalService]";
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Create hops to all configured storage nodes for the Document protocol. The "Distributor" policy resolves its
+ * recipients using slobrok lookups, so it requires no configured recipients.
+ *
+ * @param content The storage model from {@link com.yahoo.vespa.model.VespaModel}.
+ * @param table The routing table to add to.
+ */
+ private static void addContentRouting(List<ContentCluster> content, RoutingTableSpec table) {
+
+ for (ContentCluster cluster : content) {
+ RouteSpec spec = new RouteSpec(cluster.getConfigId());
+
+ if (cluster.getSearch().hasIndexedCluster()) {
+ table.addRoute(spec.addHop("[MessageType:" + cluster.getConfigId() + "]"));
+ table.addRoute(new RouteSpec(getIndexedRouteName(cluster.getConfigId()))
+ .addHop(cluster.getSearch().getIndexed().getIndexingServiceName())
+ .addHop("[Content:cluster=" + cluster.getName() + "]"));
+ table.addRoute(new RouteSpec(getDirectRouteName(cluster.getConfigId()))
+ .addHop("[Content:cluster=" + cluster.getName() + "]"));
+ } else {
+ table.addRoute(spec.addHop("[Content:cluster=" + cluster.getName() + "]"));
+ }
+ table.addRoute(new RouteSpec("storage/cluster." + cluster.getName())
+ .addHop("route:" + cluster.getConfigId()));
+ }
+ }
+
+ /**
+ * Create the "indexing" hop. This hop contains all non-streaming search clusters as recipients, and the routing
+ * policy "SearchCluster" will decide which cluster(s) are to receive every document passed through it based on a
+ * document select string derived from services.xml.
+ *
+ * @param table The routing table to add to.
+ */
+ private static void addIndexingHop(List<ContentCluster> content, RoutingTableSpec table) {
+ if (content.isEmpty()) {
+ return;
+ }
+ HopSpec hop = new HopSpec("indexing", "[DocumentRouteSelector]");
+ for (ContentCluster cluster : content) {
+ hop.addRecipient(cluster.getConfigId());
+ }
+ if (hop.hasRecipients()) {
+ table.addHop(hop);
+ }
+ }
+
+ /**
+ * Create the "default" route for the Document protocol. This route will be either a route to storage or a route to
+ * search. Since we will be supporting recovery from storage, storage takes precedence over search when deciding on
+ * the final target of the default route. If there is an unambigous docproc cluster in the application, the default
+ * route will pass through this.
+ *
+ * @param content The content model from {@link com.yahoo.vespa.model.VespaModel}.
+ * @param containerClusters a collection of {@link com.yahoo.vespa.model.container.ContainerCluster}s
+ * @param table The routing table to add to.
+ */
+ private static void addDefaultRoute(List<ContentCluster> content,
+ Collection<ContainerCluster> containerClusters,
+ RoutingTableSpec table) {
+ List<String> hops = new ArrayList<>();
+ if (!content.isEmpty()) {
+ boolean found = false;
+ for (int i = 0, len = table.getNumHops(); i < len; ++i) {
+ if (table.getHop(i).getName().equals("indexing")) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ hops.add("indexing");
+ }
+ }
+ if (!hops.isEmpty()) {
+ RouteSpec route = new RouteSpec("default");
+ String hop = getContainerClustersDocprocHop(containerClusters);
+ if (hop != null) {
+ route.addHop(hop);
+ }
+ int numHops = hops.size();
+ if (numHops == 1) {
+ route.addHop(hops.get(0));
+ } else {
+ StringBuilder str = new StringBuilder();
+ for (int i = 0; i < numHops; ++i) {
+ str.append(hops.get(i)).append(i < numHops - 1 ? " " : "");
+ }
+ route.addHop("[AND:" + str.toString() + "]");
+ }
+ table.addRoute(route);
+ }
+ }
+
+ private static String getContainerClustersDocprocHop(Collection<ContainerCluster> containerClusters) {
+ DocprocChain result = null;
+
+ for (ContainerCluster containerCluster: containerClusters) {
+ DocprocChain defaultChain = getDefaultChain(containerCluster.getDocproc());
+ if (defaultChain != null) {
+ if (result != null)
+ throw new RuntimeException("Only a single default docproc chain is allowed across all container clusters");
+
+ result = defaultChain;
+ }
+ }
+
+ return result == null ?
+ null:
+ result.getServiceName();
+ }
+
+ private static DocprocChain getDefaultChain(ContainerDocproc docproc) {
+ return docproc == null ?
+ null:
+ docproc.getChains().allChains().getComponent("default");
+ }
+
+ /**
+ * Attempts to simplify all route names by removing prefixing plugin name and whatever comes before the dot (.) in
+ * the second naming element. This can only be done to those routes that do not share primary name elements with
+ * other routes (e.g. a search clusters with the same name as a storage cluster).
+ *
+ * @param table The routing table whose route names are to be simplified.
+ */
+ private static void simplifyRouteNames(RoutingTableSpec table) {
+ if (table == null || !table.hasRoutes()) {
+ return;
+ }
+
+ // Pass 1: Determine which simplifications are in conflict.
+ Map<String, Set<String>> simple = new TreeMap<>();
+ List<String> broken = new ArrayList<>();
+ for (int i = 0, len = table.getNumRoutes(); i < len; ++i) {
+ String before = table.getRoute(i).getName();
+ String after = simplifyRouteName(before);
+ if (simple.containsKey(after)) {
+ Set<String> l = simple.get(after);
+ l.add(before);
+ if (!(l.contains("content/" + after) && l.contains("storage/cluster." + after) && (l.size() == 2))) {
+ broken.add(after);
+ }
+ } else {
+ Set<String> l = new HashSet<>();
+ l.add(before);
+ simple.put(after, l);
+ }
+ }
+
+ // Pass 2: Simplify all non-conflicting route names by alias.
+ Set<RouteSpec> alias = new HashSet<>();
+ Set<String> unique = new HashSet<>();
+ for (int i = 0; i < table.getNumRoutes(); ) {
+ RouteSpec route = table.getRoute(i);
+ String before = route.getName();
+ String after = simplifyRouteName(before);
+ if (!before.equals(after)) {
+ if (!broken.contains(after)) {
+ if (route.getNumHops() == 1 && route.getHop(0).equals(route.getName())) {
+ alias.add(new RouteSpec(after).addHop(route.getHop(0))); // full route name is redundant
+ unique.add(after);
+ table.removeRoute(i);
+ continue; // do not increment i
+ } else {
+ if (!unique.contains(after)) {
+ alias.add(new RouteSpec(after).addHop("route:" + before));
+ unique.add(after);
+ }
+ }
+ }
+ }
+ ++i;
+ }
+ for (RouteSpec rs : alias) {
+ table.addRoute(rs);
+ }
+ }
+
+ /**
+ * Returns a simplified version of the given route name. This method will remove the first component of the name as
+ * separated by a forward slash, and then remove the first component of the remaining name as separated by a dot.
+ *
+ * @param name The route name to simplify.
+ * @return The simplified route name.
+ */
+ private static String simplifyRouteName(String name) {
+ String[] foo = name.split("/", 2);
+ if (foo.length < 2) {
+ return name;
+ }
+ String[] bar = foo[1].split("\\.", 2);
+ if (bar.length < 2) {
+ return foo[1];
+ }
+ return bar[1];
+ }
+
+ @Override
+ public ApplicationSpec getApplicationSpec() {
+ return application;
+ }
+
+ @Override
+ public RoutingTableSpec getRoutingTableSpec() {
+ return routingTable;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/routing/Protocol.java b/config-model/src/main/java/com/yahoo/vespa/model/routing/Protocol.java
new file mode 100644
index 00000000000..20b503aac96
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/routing/Protocol.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.routing;
+
+import com.yahoo.messagebus.routing.ApplicationSpec;
+import com.yahoo.messagebus.routing.RoutingTableSpec;
+
+/**
+ * This interface defines the necessary api for {@link Routing} to prepare and combine routing tables for all available
+ * protocols.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface Protocol {
+
+ /**
+ * Returns the specification for the routing table of this protocol.
+ *
+ * @return The routing table spec.
+ */
+ public RoutingTableSpec getRoutingTableSpec();
+
+ /**
+ * Returns the specification of the application as seen by this protocol.
+ *
+ * @return The application spec.
+ */
+ public ApplicationSpec getApplicationSpec();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/routing/Routing.java b/config-model/src/main/java/com/yahoo/vespa/model/routing/Routing.java
new file mode 100644
index 00000000000..d5a987869e9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/routing/Routing.java
@@ -0,0 +1,201 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.routing;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.messagebus.routing.*;
+import com.yahoo.messagebus.MessagebusConfig;
+import com.yahoo.documentapi.messagebus.protocol.DocumentrouteselectorpolicyConfig;
+import java.util.*;
+
+/**
+ * This is the routing plugin of the Vespa model. This class is responsible for parsing all routing information given
+ * explicitly by the user in the optional &lt;routing&gt; element. If there is no such element, only default routes and
+ * hops will be available.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class Routing extends ConfigModel {
+
+ private final List<String> errors = new ArrayList<>();
+ private ApplicationSpec explicitApplication = null;
+ private RoutingSpec explicitRouting = null;
+ private List<Protocol> protocols = new ArrayList<>();
+ private RoutingSpec derivedRouting;
+
+ public Routing(ConfigModelContext modelContext) {
+ super(modelContext);
+ }
+
+ /**
+ * Sets the application specification to include when verifying the complete routing config. This needs to be
+ * invoked before {@link #deriveCommonSettings(com.yahoo.config.model.ConfigModelRepo)} to be included.
+ *
+ * @param app The application specification to include.
+ */
+ public void setExplicitApplicationSpec(ApplicationSpec app) {
+ explicitApplication = app;
+ }
+
+ /**
+ * Sets the routing specification to include in the derived routing config. This needs to be invoked before
+ * {@link #deriveCommonSettings(com.yahoo.config.model.ConfigModelRepo)} to be included.
+ *
+ * @param routing The routing specification to include.
+ */
+ public void setExplicitRoutingSpec(RoutingSpec routing) {
+ explicitRouting = routing;
+ }
+
+ public final List<Protocol> getProtocols() { return protocols; }
+
+ /**
+ * Derives all routing settings that can be found by inspecting the given plugin container.
+ *
+ * @param plugins All initialized plugins of the vespa model.
+ */
+ public void deriveCommonSettings(ConfigModelRepo plugins) {
+ // Combine explicit routing with protocol derived routing.
+ ApplicationSpec app = explicitApplication != null ? new ApplicationSpec(explicitApplication) : new ApplicationSpec();
+ RoutingSpec routing = explicitRouting != null ? new RoutingSpec(explicitRouting) : new RoutingSpec();
+ protocols.clear();
+ protocols.add(new DocumentProtocol(plugins));
+ for (Protocol protocol : protocols) {
+ app.add(protocol.getApplicationSpec());
+ addRoutingTable(routing, protocol.getRoutingTableSpec());
+ }
+
+ // Add default routes where appropriate, and sort content.
+ for (int i = 0, len = routing.getNumTables(); i < len; ++i) {
+ RoutingTableSpec table = routing.getTable(i);
+ if ( ! table.hasRoute("default") && table.getNumRoutes() == 1) {
+ table.addRoute(new RouteSpec("default").addHop("route:" + table.getRoute(0).getName()));
+ }
+ table.sort();
+ }
+
+ // Verify and export all produced configs.
+ errors.clear();
+ if (routing.verify(app, errors)) {
+ this.derivedRouting=routing;
+ }
+ }
+
+ public void getConfig(DocumentrouteselectorpolicyConfig.Builder builder) {
+ for (Protocol protocol : protocols) {
+ if (protocol instanceof DocumentProtocol) {
+ ((DocumentProtocol)protocol).getConfig(builder);
+ }
+ }
+ }
+
+ public void getConfig(MessagebusConfig.Builder builder) {
+ if (derivedRouting==null) {
+ // The error list should be populated then
+ return;
+ }
+ if (derivedRouting.hasTables()) {
+ for (int tableIdx = 0, numTables = derivedRouting.getNumTables(); tableIdx < numTables; ++tableIdx) {
+ RoutingTableSpec table = derivedRouting.getTable(tableIdx);
+ MessagebusConfig.Routingtable.Builder tableBuilder = new MessagebusConfig.Routingtable.Builder();
+ tableBuilder.protocol(table.getProtocol());
+ if (table.hasHops()) {
+ for (int hopIdx = 0, numHops = table.getNumHops(); hopIdx < numHops; ++hopIdx) {
+ MessagebusConfig.Routingtable.Hop.Builder hopBuilder = new MessagebusConfig.Routingtable.Hop.Builder();
+ HopSpec hop = table.getHop(hopIdx);
+ hopBuilder.name(hop.getName());
+ hopBuilder.selector(hop.getSelector());
+ if (hop.getIgnoreResult()) {
+ hopBuilder.ignoreresult(true);
+ }
+ if (hop.hasRecipients()) {
+ for (int recipientIdx = 0, numRecipients = hop.getNumRecipients();
+ recipientIdx < numRecipients; ++recipientIdx)
+ {
+ hopBuilder.recipient(hop.getRecipient(recipientIdx));
+ }
+ }
+ tableBuilder.hop(hopBuilder);
+ }
+ }
+ if (table.hasRoutes()) {
+ for (int routeIdx = 0, numRoutes = table.getNumRoutes(); routeIdx < numRoutes; ++routeIdx) {
+ MessagebusConfig.Routingtable.Route.Builder routeBuilder = new MessagebusConfig.Routingtable.Route.Builder();
+ RouteSpec route = table.getRoute(routeIdx);
+ routeBuilder.name(route.getName());
+ if (route.hasHops()) {
+ for (int hopIdx = 0, numHops = route.getNumHops(); hopIdx < numHops; ++hopIdx) {
+ routeBuilder.hop(route.getHop(hopIdx));
+ }
+ }
+ tableBuilder.route(routeBuilder);
+ }
+ }
+ builder.routingtable(tableBuilder);
+ }
+ }
+ }
+
+ /**
+ * Adds the given routing table to the given routing spec. This method will not copy hops or routes that are already
+ * defined in the target table.
+ *
+ * @param routing The routing spec to add to.
+ * @param from The table to copy content from.
+ */
+ private static void addRoutingTable(RoutingSpec routing, RoutingTableSpec from) {
+ RoutingTableSpec to = getRoutingTable(routing, from.getProtocol());
+ if (to != null) {
+ Set<String> names = new HashSet<>();
+ for (int i = 0, len = to.getNumHops(); i < len; ++i) {
+ names.add(to.getHop(i).getName());
+ }
+ for (int i = 0, len = from.getNumHops(); i < len; ++i) {
+ HopSpec hop = from.getHop(i);
+ if (!names.contains(hop.getName())) {
+ to.addHop(hop);
+ }
+ }
+
+ names.clear();
+ for (int i = 0, len = to.getNumRoutes(); i < len; ++i) {
+ names.add(to.getRoute(i).getName());
+ }
+ for (int i = 0, len = from.getNumRoutes(); i < len; ++i) {
+ RouteSpec route = from.getRoute(i);
+ if (!names.contains(route.getName())) {
+ to.addRoute(route);
+ }
+ }
+ } else {
+ routing.addTable(from);
+ }
+ }
+
+ /**
+ * Returns the routing table from the given routing spec that belongs to the named protocol.
+ *
+ * @param routing The routing whose tables to search through.
+ * @param protocol The name of the protocol whose table to return.
+ * @return The routing table found, or null.
+ */
+ private static RoutingTableSpec getRoutingTable(RoutingSpec routing, String protocol) {
+ for (int i = 0, len = routing.getNumTables(); i < len; ++i) {
+ RoutingTableSpec table = routing.getTable(i);
+ if (protocol.equals(table.getProtocol())) {
+ return table;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a list of errors found when preparing the routing configuration.
+ *
+ * @return The error list.
+ */
+ public List<String> getErrors() {
+ return Collections.unmodifiableList(errors);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/AbstractSearchCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/search/AbstractSearchCluster.java
new file mode 100644
index 00000000000..a9dd764458c
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/AbstractSearchCluster.java
@@ -0,0 +1,129 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.UserConfigRepo;
+import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
+import com.yahoo.vespa.config.search.DispatchConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import java.util.*;
+
+/**
+ * Superclass for search clusters.
+ *
+ * @author <a href="mailto:boros@yahoo-inc.com">Peter Boros</a>
+ */
+public abstract class AbstractSearchCluster extends AbstractConfigProducer
+ implements
+ DocumentdbInfoConfig.Producer,
+ IndexInfoConfig.Producer,
+ IlscriptsConfig.Producer
+{
+ private Double queryTimeout;
+ protected String clusterName;
+ protected int index;
+ private Double visibilityDelay = 0.0;
+ private List<String> documentNames = new ArrayList<>();
+
+ protected List<SearchDefinitionSpec> localSDS = new LinkedList<>();
+
+ public static final class IndexingMode {
+
+ public static final IndexingMode REALTIME = new IndexingMode("REALTIME");
+ public static final IndexingMode STREAMING = new IndexingMode("STREAMING");
+
+ public static IndexingMode createIndexingMode(String ixm) {
+ if ("REALTIME".equalsIgnoreCase(ixm)) {
+ return REALTIME;
+ } else if ("STREAMING".equalsIgnoreCase(ixm)) {
+ return STREAMING;
+ }
+ return null;
+ }
+
+ private String name;
+
+ private IndexingMode(String name) {
+ this.name = name;
+ }
+
+ public String getName() { return name; }
+
+ public String toString() {
+ return "indexingmode: " + name;
+ }
+ }
+
+ public static final class SearchDefinitionSpec {
+ private final SearchDefinition searchDefinition;
+ private final UserConfigRepo userConfigRepo;
+
+ public SearchDefinitionSpec(SearchDefinition searchDefinition,
+ UserConfigRepo userConfigRepo) {
+ this.searchDefinition = searchDefinition;
+ this.userConfigRepo = userConfigRepo;
+ }
+
+ public SearchDefinition getSearchDefinition() {
+ return searchDefinition;
+ }
+
+ public UserConfigRepo getUserConfigs() {
+ return userConfigRepo;
+ }
+ }
+
+ public AbstractSearchCluster(AbstractConfigProducer parent,
+ String clusterName, int index) {
+ super(parent, "cluster." + clusterName);
+ this.clusterName = clusterName;
+ this.index = index;
+ }
+ public void addDocumentNames(SearchDefinition searchDefinition) {
+ String dName = searchDefinition.getSearch().getDocument().getDocumentName().getName();
+ documentNames.add(dName);
+ }
+
+ /**
+ * Returns a List with document names used in this search cluster
+ * @return contained document names
+ */
+ public List<String> getDocumentNames() { return documentNames; }
+
+ public List<SearchDefinitionSpec> getLocalSDS() {
+ return localSDS;
+ }
+
+ public String getClusterName() { return clusterName; }
+ public final String getIndexingModeName() { return getIndexingMode().getName(); }
+ public final boolean isRealtime() { return getIndexingMode() == IndexingMode.REALTIME; }
+ public final boolean isStreaming() { return getIndexingMode() == IndexingMode.STREAMING; }
+ public final AbstractSearchCluster setQueryTimeout(Double to) {
+ this.queryTimeout=to;
+ return this;
+ }
+ public final AbstractSearchCluster setVisibilityDelay(double delay) {
+ this.visibilityDelay=delay;
+ return this;
+ }
+ protected abstract IndexingMode getIndexingMode();
+ public final Double getVisibilityDelay() { return visibilityDelay; }
+ public final Double getQueryTimeout() { return queryTimeout; }
+ public abstract int getRowBits();
+ public final void setClusterIndex(int index) { this.index = index; }
+ public final int getClusterIndex() { return index; }
+ protected abstract void assureSdConsistent();
+
+ @Override
+ public abstract void getConfig(DocumentdbInfoConfig.Builder builder);
+ @Override
+ public abstract void getConfig(IndexInfoConfig.Builder builder);
+ @Override
+ public abstract void getConfig(IlscriptsConfig.Builder builder);
+ public abstract void getConfig(RankProfilesConfig.Builder builder);
+ public abstract void getConfig(AttributesConfig.Builder builder);
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/Dispatch.java b/config-model/src/main/java/com/yahoo/vespa/model/search/Dispatch.java
new file mode 100644
index 00000000000..ebe43bc07db
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/Dispatch.java
@@ -0,0 +1,226 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.vespa.config.search.core.FdispatchrcConfig;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+import com.yahoo.vespa.model.content.SearchCoverage;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a dispatch (top-level (tld) or mid-level).
+ * There must be one or more tld instances in a search cluster.
+ *
+ * @author arnej27959
+ */
+@RestartConfigs({FdispatchrcConfig.class, PartitionsConfig.class})
+public class Dispatch extends AbstractService implements SearchInterface,
+ FdispatchrcConfig.Producer,
+ PartitionsConfig.Producer {
+
+ private static final String TLD_NAME = "tld";
+ private static final String DISPATCH_NAME = "dispatch";
+
+ private static final long serialVersionUID = 1L;
+ private final DispatchGroup dispatchGroup;
+ private final NodeSpec nodeSpec;
+ private final int dispatchLevel;
+ private final boolean preferLocalRow;
+ private final boolean isTopLevel;
+
+ private Dispatch(DispatchGroup dispatchGroup, AbstractConfigProducer parent, String subConfigId,
+ NodeSpec nodeSpec, int dispatchLevel, boolean preferLocalRow, boolean isTopLevel) {
+ super(parent, subConfigId);
+ this.dispatchGroup = dispatchGroup;
+ this.nodeSpec = nodeSpec;
+ this.dispatchLevel = dispatchLevel;
+ this.preferLocalRow = preferLocalRow;
+ this.isTopLevel = isTopLevel;
+ portsMeta.on(0).tag("rpc").tag("admin");
+ portsMeta.on(1).tag("fs4");
+ portsMeta.on(2).tag("http").tag("json").tag("health").tag("state");
+ setProp("clustertype", "search")
+ .setProp("clustername", dispatchGroup.getClusterName())
+ .setProp("index", nodeSpec.rowId());
+ monitorService();
+ }
+
+ public static Dispatch createTld(DispatchGroup dispatchGroup, AbstractConfigProducer parent, int rowId) {
+ return createTld(dispatchGroup, parent, rowId, false);
+ }
+
+ public static Dispatch createTld(DispatchGroup dispatchGroup, AbstractConfigProducer parent, int rowId, boolean preferLocalRow) {
+ String subConfigId = TLD_NAME + "." + rowId;
+ return new Dispatch(dispatchGroup, parent, subConfigId, new NodeSpec(rowId, 0), 0, preferLocalRow, true);
+ }
+
+ public static Dispatch createTldWithContainerIdInName(DispatchGroup dispatchGroup, AbstractConfigProducer parent, String containerName, int containerIndex) {
+ String subConfigId = containerName + "." + containerIndex + "." + TLD_NAME + "." + containerIndex;
+ return new Dispatch(dispatchGroup, parent, subConfigId, new NodeSpec(containerIndex, 0), 0, false, true);
+ }
+
+ public static Dispatch createDispatchWithStableConfigId(DispatchGroup dispatchGroup, AbstractConfigProducer parent, NodeSpec nodeSpec, int distributionKey, int dispatchLevel) {
+ String subConfigId = DISPATCH_NAME + "." + distributionKey;
+ return new Dispatch(dispatchGroup, parent, subConfigId, nodeSpec, dispatchLevel, false, false);
+ }
+
+ /**
+ * Override the default service-type
+ * @return String "topleveldispatch"
+ */
+ public String getServiceType() {
+ return "topleveldispatch";
+ }
+
+ /**
+ * @return the startup command
+ */
+ public String getStartupCommand() {
+ return "exec sbin/fdispatch -c $VESPA_CONFIG_ID";
+ }
+
+ public int getFrtPort() { return getRelativePort(0); }
+ public int getDispatchPort() { return getRelativePort(1); }
+ public @Override int getHealthPort() { return getRelativePort(2); }
+
+ /**
+ * Twice the default of the number of threads in the container.
+ * Could have been unbounded if it was not roundrobin, but stack based usage in dispatch.
+ * We are not putting to much magic into this one as this will disappear as soon as
+ * dispatch is implemented in Java.
+ */
+ public int getMaxThreads() { return 500*2; }
+
+ public String getHostname() {
+ return getHost().getHostName();
+ }
+
+ @Override
+ public NodeSpec getNodeSpec() {
+ return nodeSpec;
+ }
+
+ public String getDispatcherConnectSpec() {
+ return "tcp/" + getHost().getHostName() + ":" + getDispatchPort();
+ }
+
+ public DispatchGroup getDispatchGroup() {
+ return dispatchGroup;
+ }
+
+ @Override
+ public void getConfig(FdispatchrcConfig.Builder builder) {
+ builder.ptport(getDispatchPort()).
+ frtport(getFrtPort()).
+ healthport(getHealthPort()).
+ maxthreads(getMaxThreads());
+ if (!isTopLevel) {
+ builder.partition(getNodeSpec().partitionId());
+ builder.dispatchlevel(dispatchLevel);
+ }
+ }
+
+ @Override
+ public void getConfig(PartitionsConfig.Builder builder) {
+ int rowbits = dispatchGroup.getRowBits();
+ final PartitionsConfig.Dataset.Builder datasetBuilder = new PartitionsConfig.Dataset.Builder().
+ id(0).
+ refcost(1).
+ rowbits(rowbits).
+ numparts(dispatchGroup.getNumPartitions()).
+ mpp(dispatchGroup.getMinNodesPerColumn());
+ if (dispatchGroup.useFixedRowInDispatch()) {
+ datasetBuilder.querydistribution(PartitionsConfig.Dataset.Querydistribution.Enum.FIXEDROW);
+ datasetBuilder.maxnodesdownperfixedrow(dispatchGroup.getMaxNodesDownPerFixedRow());
+ }
+ SearchCoverage coverage = dispatchGroup.getSearchCoverage();
+ if (coverage != null) {
+ if (coverage.getMinimum() != null) {
+ datasetBuilder.minimal_searchcoverage(coverage.getMinimum() * 100); // as percentage
+ }
+ if (coverage.getMinWaitAfterCoverageFactor() != null) {
+ datasetBuilder.higher_coverage_minsearchwait(coverage.getMinWaitAfterCoverageFactor());
+ }
+ if (coverage.getMaxWaitAfterCoverageFactor() != null) {
+ datasetBuilder.higher_coverage_maxsearchwait(coverage.getMaxWaitAfterCoverageFactor());
+ }
+ }
+
+ Tuning tuning = dispatchGroup.getTuning();
+ boolean useLocalNode = false;
+ if (tuning != null && tuning.dispatch != null) {
+ useLocalNode = tuning.dispatch.useLocalNode;
+ }
+ final List<PartitionsConfig.Dataset.Engine.Builder> allEngines = new ArrayList<>();
+ for (SearchInterface searchNode : dispatchGroup.getSearchersIterable()) {
+ final PartitionsConfig.Dataset.Engine.Builder engineBuilder = new PartitionsConfig.Dataset.Engine.Builder().
+ name_and_port(searchNode.getDispatcherConnectSpec()).
+ rowid(searchNode.getNodeSpec().rowId()).
+ partid(searchNode.getNodeSpec().partitionId());
+ allEngines.add(engineBuilder);
+ if (preferLocalRow) {
+ if (getHostname().equals(searchNode.getHostName())) {
+ engineBuilder.refcost(1);
+ } else {
+ engineBuilder.refcost(Integer.MAX_VALUE);
+ }
+ }
+
+ if (!useLocalNode || getHostname().equals(searchNode.getHostName())) {
+ if (useLocalNode) {
+ engineBuilder.rowid(0);
+ }
+ datasetBuilder.engine.add(engineBuilder);
+
+ }
+ }
+ //Do not create empty engine list for a dataset if no local search nodes found
+ if(datasetBuilder.engine.isEmpty() && useLocalNode) {
+ for(PartitionsConfig.Dataset.Engine.Builder engineBuilder: allEngines) {
+ datasetBuilder.engine.add(engineBuilder);
+ }
+ }
+
+ builder.dataset.add(datasetBuilder);
+
+ if (tuning != null) {
+ tuning.getConfig(builder);
+ scaleMaxHitsPerPartitions(builder, tuning);
+ }
+ }
+
+ private int getNumLeafNodesInGroup() {
+ int numSearchers = 0;
+ for (SearchInterface search : dispatchGroup.getSearchersIterable()) {
+ if (search instanceof Dispatch) {
+ numSearchers += ((Dispatch) search).getNumLeafNodesInGroup();
+ } else {
+ numSearchers++;
+ }
+ }
+ if (numSearchers > 0) {
+ // Divide by number of partitions, otherwise we would count the same leaf node partition number of times.
+ return numSearchers / dispatchGroup.getNumPartitions();
+ }
+ return 0;
+ }
+
+ private void scaleMaxHitsPerPartitions(PartitionsConfig.Builder builder, Tuning tuning) {
+ if (tuning == null || tuning.dispatch == null || tuning.dispatch.maxHitsPerPartition == null) {
+ return;
+ }
+ int numLeafNodes = getNumLeafNodesInGroup();
+ for (PartitionsConfig.Dataset.Builder dataset : builder.dataset) {
+ dataset.maxhitspernode(tuning.dispatch.maxHitsPerPartition * numLeafNodes);
+ }
+ }
+
+ /**
+ * @return the number of ports needed
+ */
+ public int getPortCount() { return 3; }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroup.java b/config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroup.java
new file mode 100644
index 00000000000..e9d01f50fb6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroup.java
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.vespa.model.content.SearchCoverage;
+
+import java.util.*;
+
+/**
+ * Class representing a group of @link{SearchInterface} nodes and a set of @link{Dispatch} nodes.
+ *
+ * Each @link{Dispatch} has a reference to an instance of this class and use it when producing config.
+ *
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class DispatchGroup {
+
+ private final List<Dispatch> dispatchers = new ArrayList<>();
+ private final Map<Integer, Map<Integer, SearchInterface> > searchers = new TreeMap<>();
+
+ final private IndexedSearchCluster sc;
+
+ public DispatchGroup(IndexedSearchCluster sc) {
+ this.sc = sc;
+ }
+
+ DispatchGroup addDispatcher(Dispatch dispatch) {
+ dispatchers.add(dispatch);
+ return this;
+ }
+
+ DispatchGroup addSearcher(SearchInterface search) {
+ Map<Integer, SearchInterface> rows = searchers.get(search.getNodeSpec().partitionId());
+ if (rows == null) {
+ rows = new TreeMap<>();
+ rows.put(search.getNodeSpec().rowId(), search);
+ searchers.put(search.getNodeSpec().partitionId(), rows);
+ } else {
+ if (rows.containsKey(search.getNodeSpec().rowId())) {
+ throw new IllegalArgumentException("Already contains a search node with row id '" + search.getNodeSpec().rowId() + "'");
+ }
+ rows.put(search.getNodeSpec().rowId(), search);
+ }
+ return this;
+ }
+
+ DispatchGroup clearSearchers() {
+ searchers.clear();
+ return this;
+ }
+
+ List<Dispatch> getDispatchers() {
+ return Collections.unmodifiableList(dispatchers);
+ }
+
+ public Iterable getSearchersIterable() {
+ return new Iterable(searchers);
+ }
+
+ public int getRowBits() {
+ return sc.getRowBits();
+ }
+
+ public int getNumPartitions() {
+ return searchers.size();
+ }
+
+ public boolean useFixedRowInDispatch() {
+ return sc.useFixedRowInDispatch();
+ }
+
+ public int getMinNodesPerColumn() {
+ return sc.getMinNodesPerColumn();
+ }
+
+ public int getMaxNodesDownPerFixedRow() {
+ return sc.getMaxNodesDownPerFixedRow();
+ }
+
+ SearchCoverage getSearchCoverage() {
+ return sc.getSearchCoverage();
+ }
+
+ Tuning getTuning() {
+ return sc.getTuning();
+ }
+
+ String getClusterName() {
+ return sc.getClusterName();
+ }
+
+ static class Iterator implements java.util.Iterator<SearchInterface> {
+ private java.util.Iterator<Map<Integer, SearchInterface>> it1;
+ private java.util.Iterator<SearchInterface> it2;
+ Iterator(Map<Integer, Map<Integer, SearchInterface> > s) {
+ it1 = s.values().iterator();
+ if (it1.hasNext()) {
+ it2 = it1.next().values().iterator();
+ }
+ }
+ @Override
+ public boolean hasNext() {
+ if (it2 == null) {
+ return false;
+ }
+ while (!it2.hasNext() && it1.hasNext()) {
+ it2 = it1.next().values().iterator();
+ }
+ return it2.hasNext();
+ }
+
+ @Override
+ public SearchInterface next() {
+ return it2.next();
+ }
+
+ @Override
+ public void remove() {
+ throw new IllegalStateException("'remove' not implemented");
+ }
+ }
+
+ public static class Iterable implements java.lang.Iterable<SearchInterface> {
+ final Map<Integer, Map<Integer, SearchInterface> > searchers;
+ Iterable(Map<Integer, Map<Integer, SearchInterface> > searchers) { this.searchers = searchers; }
+ @Override
+ public java.util.Iterator<SearchInterface> iterator() {
+ return new Iterator(searchers);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroupBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroupBuilder.java
new file mode 100644
index 00000000000..9b1f63e813e
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/DispatchGroupBuilder.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.SimpleConfigProducer;
+import com.yahoo.vespa.model.content.DispatchSpec;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class used to build the mid-level dispatch groups in an indexed content cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class DispatchGroupBuilder {
+
+ private final SimpleConfigProducer dispatchParent;
+ private final DispatchGroup rootDispatch;
+ private final IndexedSearchCluster searchCluster;
+
+ public DispatchGroupBuilder(SimpleConfigProducer dispatchParent,
+ DispatchGroup rootDispatch,
+ IndexedSearchCluster searchCluster) {
+ this.dispatchParent = dispatchParent;
+ this.rootDispatch = rootDispatch;
+ this.searchCluster = searchCluster;
+ }
+
+ public void build(List<DispatchSpec.Group> groupsSpec,
+ List<SearchNode> searchNodes) {
+ Map<Integer, SearchNode> searchNodeMap = buildSearchNodeMap(searchNodes);
+ for (int partId = 0; partId < groupsSpec.size(); ++partId) {
+ DispatchSpec.Group groupSpec = groupsSpec.get(partId);
+ DispatchGroup group = new DispatchGroup(searchCluster);
+ populateDispatchGroup(group, groupSpec.getNodes(), searchNodeMap, partId);
+ }
+ }
+
+ private void populateDispatchGroup(DispatchGroup group,
+ List<DispatchSpec.Node> nodeList,
+ Map<Integer, SearchNode> searchNodesMap,
+ int partId) {
+ for (int rowId = 0; rowId < nodeList.size(); ++rowId) {
+ int distributionKey = nodeList.get(rowId).getDistributionKey();
+ SearchNode searchNode = searchNodesMap.get(distributionKey);
+ Dispatch dispatch = buildDispatch(group, new NodeSpec(rowId, partId), distributionKey, searchNode.getHostResource());
+ group.addDispatcher(dispatch);
+ rootDispatch.addSearcher(dispatch);
+
+ // Note: the rowId in this context will be the partId for the underlying search node.
+ group.addSearcher(buildSearchInterface(searchNode, rowId));
+ }
+ }
+
+ /**
+ * Builds a mid-level dispatcher with a configId containing the same stable distribution-key as the search node it
+ * is located on.
+ *
+ * If this.dispatchParent has subConfigId 'dispatchers', the config ids of the mid-level
+ * dispatchers are '../dispatchers/dispatch.X' where X is the distribution-key of the search node.
+ *
+ * The dispatch group that will contain this mid-level dispatcher is no longer part of the config producer tree,
+ * but only contains information about the dispatchers and searchers in this group.
+ */
+ private Dispatch buildDispatch(DispatchGroup group, NodeSpec nodeSpec, int distributionKey, HostResource hostResource) {
+ Dispatch dispatch = Dispatch.createDispatchWithStableConfigId(group, dispatchParent, nodeSpec, distributionKey, 1);
+ dispatch.setHostResource(hostResource);
+ dispatch.initService();
+ return dispatch;
+ }
+
+ private static SearchInterface buildSearchInterface(SearchNode searchNode, int partId) {
+ searchNode.updatePartition(partId); // ensure that search node uses the same partId as dispatch sees
+ return new SearchNodeWrapper(new NodeSpec(0, partId), searchNode);
+ }
+
+ private static Map<Integer, SearchNode> buildSearchNodeMap(List<SearchNode> searchNodes) {
+ Map<Integer, SearchNode> retval = new LinkedHashMap<>();
+ for (SearchNode node : searchNodes) {
+ retval.put(node.getDistributionKey(), node);
+ }
+ return retval;
+ }
+
+ public static List<DispatchSpec.Group> createDispatchGroups(List<SearchNode> searchNodes,
+ int numDispatchGroups) {
+ if (numDispatchGroups > searchNodes.size())
+ numDispatchGroups = searchNodes.size();
+
+ List<DispatchSpec.Group> groupsSpec = new ArrayList<>();
+ int numNodesPerGroup = searchNodes.size() / numDispatchGroups;
+ if (searchNodes.size() % numDispatchGroups != 0) {
+ numNodesPerGroup += 1;
+ }
+ int searchNodeIdx = 0;
+ for (int i = 0; i < numDispatchGroups; ++i) {
+ DispatchSpec.Group groupSpec = new DispatchSpec.Group();
+ for (int j = 0; j < numNodesPerGroup && searchNodeIdx < searchNodes.size(); ++j) {
+ groupSpec.addNode(new DispatchSpec.Node(searchNodes.get(searchNodeIdx++).getDistributionKey()));
+ }
+ groupsSpec.add(groupSpec);
+ }
+ assert(searchNodeIdx == searchNodes.size());
+ return groupsSpec;
+ }
+}
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/DocumentDatabase.java b/config-model/src/main/java/com/yahoo/vespa/model/search/DocumentDatabase.java
new file mode 100644
index 00000000000..d44f81d571b
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/DocumentDatabase.java
@@ -0,0 +1,90 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.vespa.config.search.SummaryConfig;
+import com.yahoo.vespa.config.search.IndexschemaConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.vespa.config.search.SummarymapConfig;
+import com.yahoo.vespa.config.search.summary.JuniperrcConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+
+/**
+ * Represents a document database and the backend configuration needed for this database.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class DocumentDatabase extends AbstractConfigProducer implements
+ IndexInfoConfig.Producer,
+ IlscriptsConfig.Producer,
+ AttributesConfig.Producer,
+ RankProfilesConfig.Producer,
+ IndexschemaConfig.Producer,
+ JuniperrcConfig.Producer,
+ SummarymapConfig.Producer,
+ SummaryConfig.Producer {
+
+ private final String inputDocType;
+ private final DerivedConfiguration derivedCfg;
+
+ public DocumentDatabase(AbstractConfigProducer parent, String inputDocType, DerivedConfiguration derivedCfg) {
+ super(parent, inputDocType);
+ this.inputDocType = inputDocType;
+ this.derivedCfg = derivedCfg;
+ }
+
+ public String getName() {
+ return inputDocType;
+ }
+
+ public String getInputDocType() {
+ return inputDocType;
+ }
+
+ public DerivedConfiguration getDerivedConfiguration() {
+ return derivedCfg;
+ }
+
+ @Override
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ derivedCfg.getIndexInfo().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(IlscriptsConfig.Builder builder) {
+ derivedCfg.getIndexingScript().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ derivedCfg.getAttributeFields().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ derivedCfg.getRankProfileList().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(IndexschemaConfig.Builder builder) {
+ derivedCfg.getIndexSchema().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(JuniperrcConfig.Builder builder) {
+ derivedCfg.getJuniperrc().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(SummarymapConfig.Builder builder) {
+ derivedCfg.getSummaryMap().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(SummaryConfig.Builder builder) {
+ derivedCfg.getSummaries().getConfig(builder);
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/DocumentSelectionConverter.java b/config-model/src/main/java/com/yahoo/vespa/model/search/DocumentSelectionConverter.java
new file mode 100644
index 00000000000..942b389fce9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/DocumentSelectionConverter.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.document.select.*;
+import com.yahoo.document.select.convert.SelectionExpressionConverter;
+import com.yahoo.document.select.parser.ParseException;
+import java.util.Map;
+
+
+/**
+ * @author <a href="mailto:lulf@yahoo-inc.com">Ulf Lilleengen</a>
+ */
+public class DocumentSelectionConverter {
+
+ private final DocumentSelector selector;
+ private final Map<String, String> queryExpressionMap;
+
+ public DocumentSelectionConverter(String selection) throws ParseException, UnsupportedOperationException, IllegalArgumentException {
+ this.selector = new DocumentSelector(selection);
+ NowCheckVisitor nowChecker = new NowCheckVisitor();
+ selector.visit(nowChecker);
+ if (nowChecker.requiresConversion()) {
+ SelectionExpressionConverter converter = new SelectionExpressionConverter();
+ selector.visit(converter);
+ this.queryExpressionMap = converter.getQueryMap();
+ } else {
+ this.queryExpressionMap = null;
+ }
+ }
+
+ /**
+ * Transforms the selection into a search query.
+ * @return A search query representing the selection.
+ */
+ public String getQuery(String documentType) {
+ if (queryExpressionMap == null)
+ return null;
+ if (!queryExpressionMap.containsKey(documentType))
+ return null;
+ return queryExpressionMap.get(documentType);
+ }
+
+ /**
+ * Transforms the selection into an inverted search query.
+ * @return A search query representing the selection.
+ */
+ public String getInvertedQuery(String documentType) {
+ String query = getQuery(documentType);
+ if (query == null)
+ return null;
+ return query.replaceAll(">", "<");
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/IndexedElasticSearchCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexedElasticSearchCluster.java
new file mode 100644
index 00000000000..88ebef9da86
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexedElasticSearchCluster.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * @author balder
+ */
+public class IndexedElasticSearchCluster extends IndexedSearchCluster {
+
+ public IndexedElasticSearchCluster(AbstractConfigProducer parent, String clusterName, int index) {
+ super(parent, clusterName, index);
+ }
+
+ @Override
+ public boolean getAllowFeedingWhenNodesDown() {
+ return true;
+ }
+
+ @Override
+ public int getMinNodesPerColumn() { return 0; }
+
+ @Override
+ protected void assureSdConsistent() { }
+
+ @Override
+ public int getRowBits() { return 8; }
+
+ @Override
+ boolean useFixedRowInDispatch() {
+ for (SearchNode node : getSearchNodes()) {
+ if (node.getNodeSpec().rowId() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isElastic() { return true; }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/IndexedSearchCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexedSearchCluster.java
new file mode 100644
index 00000000000..aa664264ba6
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexedSearchCluster.java
@@ -0,0 +1,390 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.vespa.config.search.DispatchConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.searchdefinition.UnproperSearch;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.SimpleConfigProducer;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.content.DispatchSpec;
+import com.yahoo.vespa.model.content.SearchCoverage;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public abstract class IndexedSearchCluster extends SearchCluster
+ implements
+ DocumentdbInfoConfig.Producer,
+ // TODO consider removing, these only produced by UnionConfiguration and DocumentDatabase?
+ IndexInfoConfig.Producer,
+ IlscriptsConfig.Producer,
+ DispatchConfig.Producer
+{
+
+ /**
+ * Class used to retrieve combined configuration from multiple document databases.
+ * It is not a {@link com.yahoo.config.ConfigInstance.Producer} of those configs,
+ * that is handled (by delegating to this) by the {@link IndexedSearchCluster}
+ * which is the parent to this. This avoids building the config multiple times.
+ */
+ public static class UnionConfiguration
+ extends AbstractConfigProducer
+ implements AttributesConfig.Producer {
+ private final List<DocumentDatabase> docDbs;
+
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ for (DocumentDatabase docDb : docDbs) {
+ docDb.getConfig(builder);
+ }
+ }
+
+ public void getConfig(IlscriptsConfig.Builder builder) {
+ for (DocumentDatabase docDb : docDbs) {
+ docDb.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ for (DocumentDatabase docDb : docDbs) {
+ docDb.getConfig(builder);
+ }
+ }
+
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ for (DocumentDatabase docDb : docDbs) {
+ docDb.getConfig(builder);
+ }
+ }
+
+ public UnionConfiguration(AbstractConfigProducer parent, List<DocumentDatabase> docDbs) {
+ super(parent, "union");
+ this.docDbs = docDbs;
+ }
+ }
+
+ private static final Logger log = Logger.getLogger(IndexedSearchCluster.class.getName());
+
+ private String indexingClusterName = null; // The name of the docproc cluster to run indexing, by config.
+ private String indexingChainName = null;
+
+ private DocprocChain indexingChain; // The actual docproc chain indexing for this.
+
+ private Tuning tuning;
+ private SearchCoverage searchCoverage;
+
+ // This is the document selector string as derived from the subscription tag.
+ private String routingSelector = null;
+ private DocumentSelectionConverter selectionConverter = null;
+ private List<DocumentDatabase> documentDbs = new LinkedList<>();
+ private final UnionConfiguration unionCfg;
+ private int maxNodesDownPerFixedRow = 0;
+
+ private final SimpleConfigProducer dispatchParent;
+ private final DispatchGroup rootDispatch;
+ private DispatchSpec dispatchSpec;
+ private List<SearchNode> searchNodes = new ArrayList<>();
+
+ /**
+ * Returns the document selector that is able to resolve what documents are to be routed to this search cluster.
+ * This string uses the document selector language as defined in the "document" module.
+ *
+ * @return The document selector.
+ */
+ public String getRoutingSelector() {
+ return routingSelector;
+ }
+
+ public IndexedSearchCluster(AbstractConfigProducer parent, String clusterName, int index) {
+ super(parent, clusterName, index);
+ unionCfg = new UnionConfiguration(this, documentDbs);
+ dispatchParent = new SimpleConfigProducer(this, "dispatchers");
+ rootDispatch = new DispatchGroup(this);
+ }
+
+ @Override
+ protected IndexingMode getIndexingMode() { return IndexingMode.REALTIME; }
+
+ public final boolean hasExplicitIndexingCluster() {
+ return indexingClusterName != null;
+ }
+
+ public final boolean hasExplicitIndexingChain() {
+ return indexingChainName != null;
+ }
+
+ /**
+ * Returns the name of the docproc cluster running indexing for this search cluster. This is derived from the
+ * services file on initialization, this can NOT be used at runtime to determine indexing chain. When initialization
+ * is done, the {@link #getIndexingServiceName()} method holds the actual indexing docproc chain object.
+ *
+ * @return The name of the docproc cluster associated with this.
+ */
+ public String getIndexingClusterName() {
+ return hasExplicitIndexingCluster() ? indexingClusterName : getClusterName() + ".indexing";
+ }
+
+ public String getIndexingChainName() {
+ return indexingChainName;
+ }
+
+ public void setIndexingChainName(String indexingChainName) {
+ this.indexingChainName = indexingChainName;
+ }
+
+ /**
+ * Sets the name of the docproc cluster running indexing for this search cluster. This is for initial configuration,
+ * and will not reflect the actual indexing chain. See {@link #getIndexingClusterName} for more detail.
+ *
+ * @param name The name of the docproc cluster associated with this.
+ */
+ public void setIndexingClusterName(String name) {
+ indexingClusterName = name;
+ }
+
+ public String getIndexingServiceName() {
+ return indexingChain.getServiceName();
+ }
+
+ /**
+ * Sets the docproc chain that will be running indexing for this search cluster. This is set by the
+ * {@link com.yahoo.vespa.model.content.Content} model during build.
+ *
+ * @param chain the chain that is to run indexing for this cluster.
+ * @return this, to allow chaining.
+ */
+ public AbstractSearchCluster setIndexingChain(DocprocChain chain) {
+ indexingChain = chain;
+ return this;
+ }
+
+ public Dispatch addTld(AbstractConfigProducer tldParent, HostResource hostResource) {
+ int index = rootDispatch.getDispatchers().size();
+ Dispatch tld = Dispatch.createTld(rootDispatch, tldParent, index);
+ tld.setHostResource(hostResource);
+ tld.initService();
+ rootDispatch.addDispatcher(tld);
+ return tld;
+ }
+
+ /**
+ * Make sure to allocate tld with same id as container (i.e if container cluster name is 'foo', with containers
+ * with index 0,1,2 the tlds created will get names ../foo.0.tld.0, ../foo.1.tld.1, ../foo.2.tld.2, so that tld config id is
+ * stable no matter what changes are done to the number of containers in a container cluster
+ * @param tldParent the indexed search cluster the tlds to add should be connected to
+ * @param containerCluster the container cluster that should use the tlds created for searching the indexed search cluster above
+ */
+ public void addTldsWithSameIdsAsContainers(AbstractConfigProducer tldParent, ContainerCluster containerCluster) {
+ for (Container container : containerCluster.getContainers()) {
+ final String containerSubId = container.getSubId();
+ if (!containerSubId.contains(".")) {
+ throw new RuntimeException("Expected container sub id to be of the form string.number");
+ }
+ int containerIndex = Integer.parseInt(containerSubId.split("\\.")[1]);
+ final String containerClusterName = containerCluster.getName();
+ log.log(LogLevel.DEBUG, "Adding tld with index " + containerIndex + " for content cluster " + this.getClusterName() +
+ ", container cluster " + containerClusterName + " (container id " + containerSubId + ") on host " + container.getHostResource().getHostName());
+ rootDispatch.addDispatcher(createTld(tldParent, container.getHostResource(), containerClusterName, containerIndex));
+ }
+ }
+
+ public Dispatch createTld(AbstractConfigProducer tldParent, HostResource hostResource, String containerClusterName, int containerIndex) {
+ Dispatch tld = Dispatch.createTldWithContainerIdInName(rootDispatch, tldParent, containerClusterName, containerIndex);
+ tld.setHostResource(hostResource);
+ tld.initService();
+ return tld;
+ }
+
+ public DispatchGroup getRootDispatch() { return rootDispatch; }
+
+ public void addSearcher(SearchNode searcher) {
+ searchNodes.add(searcher);
+ rootDispatch.addSearcher(searcher);
+ }
+
+ public List<Dispatch> getTLDs() { return rootDispatch.getDispatchers(); }
+
+ public List<SearchNode> getSearchNodes() { return Collections.unmodifiableList(searchNodes); }
+ public int getSearchNodeCount() { return searchNodes.size(); }
+ public SearchNode getSearchNode(int index) { return searchNodes.get(index); }
+ public void setTuning(Tuning tuning) {
+ this.tuning = tuning;
+ }
+ public Tuning getTuning() { return tuning; }
+
+ public void fillDocumentDBConfig(String documentType, ProtonConfig.Documentdb.Builder builder) {
+ for (DocumentDatabase sdoc : documentDbs) {
+ if (sdoc.getName().equals(documentType)) {
+ fillDocumentDBConfig(sdoc, builder);
+ return;
+ }
+ }
+ }
+
+ protected void fillDocumentDBConfig(DocumentDatabase sdoc, ProtonConfig.Documentdb.Builder ddbB) {
+ ddbB.inputdoctypename(sdoc.getInputDocType())
+ .configid(sdoc.getConfigId())
+ .visibilitydelay(getVisibilityDelay());
+ }
+
+ @Override
+ public void getConfig(DocumentdbInfoConfig.Builder builder) {
+ for (DocumentDatabase db : documentDbs) {
+ DocumentdbInfoConfig.Documentdb.Builder docDb = new DocumentdbInfoConfig.Documentdb.Builder();
+ docDb.name(db.getName());
+ convertSummaryConfig(db, db, docDb);
+ RankProfilesConfig.Builder rpb = new RankProfilesConfig.Builder();
+ db.getConfig(rpb);
+ addRankProfilesConfig(docDb, new RankProfilesConfig(rpb));
+ builder.documentdb(docDb);
+ }
+ }
+
+ public void setRoutingSelector(String sel) {
+ this.routingSelector=sel;
+ if (this.routingSelector != null) {
+ try {
+ this.selectionConverter = new DocumentSelectionConverter(this.routingSelector);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid routing selector: " + e.getMessage());
+ }
+ }
+ }
+ /**
+ * Create default config if not specified by user.
+ * Accept empty strings as user config - it means that all feeds/documents are accepted.
+ */
+ public void defaultDocumentsConfig() {
+ if ((routingSelector == null) && !getDocumentNames().isEmpty()) {
+ Iterator<String> it = getDocumentNames().iterator();
+ routingSelector = it.next();
+ while (it.hasNext()) {
+ routingSelector += " or " + it.next();
+ }
+ }
+ }
+ protected void deriveAllSearchDefinitions(List<SearchDefinitionSpec> localSearches,
+ List<com.yahoo.searchdefinition.Search> globalSearches) {
+ for (SearchDefinitionSpec spec : localSearches) {
+ com.yahoo.searchdefinition.Search search = spec.getSearchDefinition().getSearch();
+ if (!(search instanceof UnproperSearch)) {
+ DocumentDatabase db = new DocumentDatabase(this, search.getName(), new DerivedConfiguration(search, globalSearches, deployLogger(), getRoot().getDeployState().rankProfileRegistry()));
+ // TODO: remove explicit adding of user configs when the complete content model is built using builders.
+ db.mergeUserConfigs(spec.getUserConfigs());
+ documentDbs.add(db);
+ }
+ }
+ }
+
+ public List<DocumentDatabase> getDocumentDbs() {
+ return documentDbs;
+ }
+
+ public void setSearchCoverage(SearchCoverage searchCoverage) {
+ this.searchCoverage = searchCoverage;
+ }
+
+ public SearchCoverage getSearchCoverage() {
+ return searchCoverage;
+ }
+
+ @Override
+ public DerivedConfiguration getSdConfig() { return null; }
+
+ @Override
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ unionCfg.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(IlscriptsConfig.Builder builder) {
+ unionCfg.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ unionCfg.getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ unionCfg.getConfig(builder);
+ }
+
+ @Override
+ protected void exportSdFiles(File toDir) throws IOException { }
+
+ public abstract boolean getAllowFeedingWhenNodesDown();
+
+ public abstract int getMinNodesPerColumn();
+
+ boolean useFixedRowInDispatch() {
+ return false;
+ }
+
+ public abstract boolean isElastic();
+
+ int getMaxNodesDownPerFixedRow() {
+ return maxNodesDownPerFixedRow;
+ }
+
+ public void setMaxNodesDownPerFixedRow(int value) {
+ maxNodesDownPerFixedRow = value;
+ }
+
+ public void setDispatchSpec(DispatchSpec dispatchSpec) {
+ if (dispatchSpec.getNumDispatchGroups() != null) {
+ this.dispatchSpec = new DispatchSpec.Builder().setGroups
+ (DispatchGroupBuilder.createDispatchGroups(getSearchNodes(),
+ dispatchSpec.getNumDispatchGroups())).build();
+ } else {
+ this.dispatchSpec = dispatchSpec;
+ }
+ }
+
+ public DispatchSpec getDispatchSpec() {
+ return dispatchSpec;
+ }
+
+ public boolean useMultilevelDispatchSetup() {
+ return dispatchSpec != null && dispatchSpec.getGroups() != null && !dispatchSpec.getGroups().isEmpty();
+ }
+
+ public void setupDispatchGroups() {
+ if (!useMultilevelDispatchSetup()) {
+ return;
+ }
+ rootDispatch.clearSearchers();
+ new DispatchGroupBuilder(dispatchParent, rootDispatch, this).build(dispatchSpec.getGroups(), getSearchNodes());
+ }
+
+ @Override
+ public void getConfig(DispatchConfig.Builder builder) {
+ for (SearchNode node : getSearchNodes()) {
+ DispatchConfig.Node.Builder nodeBuilder = new DispatchConfig.Node.Builder();
+ nodeBuilder.key(node.getDistributionKey());
+ nodeBuilder.host(node.getHostName());
+ nodeBuilder.port(node.getRpcPort());
+ builder.node(nodeBuilder);
+ }
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/IndexingDocprocChain.java b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexingDocprocChain.java
new file mode 100644
index 00000000000..c6fb6ef4b71
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexingDocprocChain.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.vespa.configdefinition.SpecialtokensConfig;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class IndexingDocprocChain extends DocprocChain implements SpecialtokensConfig.Producer {
+
+ public static final String NAME = "indexing";
+ private static final List<Phase> phases = new ArrayList<>(2);
+
+ static {
+ phases.add(new Phase("indexingStart", Collections.<String>emptySet(), Collections.<String>emptySet()));
+ phases.add(new Phase("indexingEnd", Collections.<String>emptySet(), Collections.<String>emptySet()));
+ }
+
+ public IndexingDocprocChain() {
+ super(new ChainSpecification(new ComponentId(NAME),
+ new ChainSpecification.Inheritance(Collections.<ComponentSpecification>emptySet(),
+ Collections.<ComponentSpecification>emptySet()),
+ phases,
+ Collections.<ComponentSpecification>emptySet()),
+ new HashMap<>());
+ addInnerComponent(new IndexingProcessor());
+ }
+
+ @Override
+ public void getConfig(SpecialtokensConfig.Builder builder) {
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/IndexingProcessor.java b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexingProcessor.java
new file mode 100644
index 00000000000..89f2ebbe5eb
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/IndexingProcessor.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.vespa.model.container.docproc.DocumentProcessor;
+import com.yahoo.vespa.model.container.docproc.model.DocumentProcessorModel;
+
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class IndexingProcessor extends DocumentProcessor {
+
+ public static final String docprocsBundleSpecification = "docprocs";
+
+ public IndexingProcessor() {
+ super(new DocumentProcessorModel(new BundleInstantiationSpecification(new ComponentId(DocumentProcessor.INDEXER),
+ new ComponentSpecification(DocumentProcessor.INDEXER),
+ new ComponentSpecification(docprocsBundleSpecification)),
+ new Dependencies(Collections.<String>emptyList(),
+ Collections.<String>emptyList(),
+ Collections.<String>emptyList()),
+ new HashMap<>()));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/MultilevelDispatchValidator.java b/config-model/src/main/java/com/yahoo/vespa/model/search/MultilevelDispatchValidator.java
new file mode 100644
index 00000000000..a068d173ad8
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/MultilevelDispatchValidator.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.vespa.model.content.DispatchSpec;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Class used to validate that multilevel dispatch is correctly setup in an indexed content cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class MultilevelDispatchValidator {
+
+ private final String clusterName;
+ private final DispatchSpec dispatchSpec;
+ private final List<SearchNode> searchNodes;
+
+ public MultilevelDispatchValidator(String clusterName,
+ DispatchSpec dispatchSpec,
+ List<SearchNode> searchNodes) {
+ this.clusterName = clusterName;
+ this.dispatchSpec = dispatchSpec;
+ this.searchNodes = searchNodes;
+ }
+
+ public void validate() {
+ validateThatWeReferenceNodesOnlyOnce();
+ validateThatWeReferenceAllNodes();
+ validateThatWeUseValidNodeReferences();
+ }
+
+ private void validateThatWeReferenceNodesOnlyOnce() {
+ Set<Integer> distKeys = new HashSet<>();
+ for (DispatchSpec.Group group : dispatchSpec.getGroups()) {
+ for (DispatchSpec.Node node : group.getNodes()) {
+ int distKey = node.getDistributionKey();
+ if (distKeys.contains(distKey)) {
+ throw new IllegalArgumentException(getErrorMsgPrefix() + "Expected nodes to be referenced only once in dispatch groups, but node with distribution key '" + distKey + "' is referenced multiple times.");
+ }
+ distKeys.add(distKey);
+ }
+ }
+ }
+
+ private void validateThatWeReferenceAllNodes() {
+ Set<Integer> distKeys = createDistributionKeysSet();
+ for (DispatchSpec.Group group : dispatchSpec.getGroups()) {
+ for (DispatchSpec.Node node : group.getNodes()) {
+ distKeys.remove(node.getDistributionKey());
+ }
+ }
+ if (!distKeys.isEmpty()) {
+ Object[] sorted = distKeys.toArray();
+ Arrays.sort(sorted);
+ throw new IllegalArgumentException(getErrorMsgPrefix() + "Expected all nodes to be referenced in dispatch groups, but " + distKeys.size() +
+ " node(s) with distribution keys " + Arrays.toString(sorted) + " are not referenced.");
+ }
+ }
+
+ private void validateThatWeUseValidNodeReferences() {
+ Set<Integer> distKeys = createDistributionKeysSet();
+ for (DispatchSpec.Group group : dispatchSpec.getGroups()) {
+ for (DispatchSpec.Node node : group.getNodes()) {
+ int distKey = node.getDistributionKey();
+ if (!distKeys.contains(distKey)) {
+ throw new IllegalArgumentException(getErrorMsgPrefix() + "Expected all node references in dispatch groups to reference existing nodes, " +
+ "but node with distribution key '" + distKey + "' does not exists.");
+ }
+ }
+ }
+ }
+
+ private Set<Integer> createDistributionKeysSet() {
+ Set<Integer> distKeys = new HashSet<>();
+ for (SearchNode node : searchNodes) {
+ distKeys.add(node.getDistributionKey());
+ }
+ return distKeys;
+ }
+
+ private String getErrorMsgPrefix() {
+ return "In indexed content cluster '" + clusterName + "': ";
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/NodeSpec.java b/config-model/src/main/java/com/yahoo/vespa/model/search/NodeSpec.java
new file mode 100644
index 00000000000..e5759d8c2e5
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/NodeSpec.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+/**
+ * Represents the row id and partition id of a search interface node.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class NodeSpec {
+
+ private final int rowId;
+ private final int partitionId;
+
+ public NodeSpec(int rowId, int partitionId) {
+ if (rowId < 0) {
+ throw new IllegalArgumentException("RowId(" + rowId + ") can not be below 0");
+ }
+ if (partitionId < 0) {
+ throw new IllegalArgumentException("PartId(" + partitionId + ") can not be below 0");
+ }
+ this.rowId = rowId;
+ this.partitionId = partitionId;
+ }
+
+ public int rowId() {
+ return rowId;
+ }
+
+ public int partitionId() {
+ return partitionId;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/SearchCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchCluster.java
new file mode 100644
index 00000000000..c88b40c8c3d
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchCluster.java
@@ -0,0 +1,153 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.searchdefinition.derived.RawRankProfile;
+import com.yahoo.searchdefinition.derived.SummaryMap;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.vespa.config.search.SummaryConfig;
+import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
+import com.yahoo.vespa.config.search.SummarymapConfig;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Represents a search cluster.
+ *
+ * @author arnej27959
+ */
+public abstract class SearchCluster extends AbstractSearchCluster
+ implements
+ DocumentdbInfoConfig.Producer,
+ IndexInfoConfig.Producer,
+ IlscriptsConfig.Producer {
+
+ private static final long serialVersionUID = 1L;
+
+ protected SearchCluster(AbstractConfigProducer parent, String clusterName, int index) {
+ super(parent, clusterName, index);
+ }
+
+ public void writeFiles(File directory) throws java.io.IOException {
+ if (!directory.isDirectory() && !directory.mkdirs()) {
+ throw new java.io.IOException("Cannot create directory: "+ directory);
+ }
+ writeSdFiles(directory);
+ super.writeFiles(directory);
+ }
+
+ /**
+ * Must be called after cluster is built, to derive SD configs
+ * Derives the search definitions from the application package..
+ * Also stores the document names contained in the search
+ * definitions.
+ */
+ public void deriveSearchDefinitions(List<com.yahoo.searchdefinition.Search> global) {
+ deriveAllSearchDefinitions(getLocalSDS(), global);
+ }
+
+ @Override
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ if (getSdConfig()!=null) getSdConfig().getIndexInfo().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(IlscriptsConfig.Builder builder) {
+ if (getSdConfig()!=null) getSdConfig().getIndexingScript().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ if (getSdConfig()!=null) getSdConfig().getAttributeFields().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ if (getSdConfig()!=null) getSdConfig().getRankProfileList().getConfig(builder);
+ }
+
+ /**
+ * Converst summary and summary map config to the appropriate information in documentdb
+ *
+ * @param summaryConfigProducer the summary config
+ * @param summarymapConfigProducer the summary map config, or null if none is available
+ * @param docDb the target document dm config
+ */
+ protected void convertSummaryConfig(SummaryConfig.Producer summaryConfigProducer,
+ SummarymapConfig.Producer summarymapConfigProducer,
+ DocumentdbInfoConfig.Documentdb.Builder docDb) {
+
+ SummaryConfig.Builder summaryConfigBuilder = new SummaryConfig.Builder();
+ summaryConfigProducer.getConfig(summaryConfigBuilder);
+ SummaryConfig summaryConfig = new SummaryConfig(summaryConfigBuilder);
+
+ SummarymapConfig summarymapConfig = null;
+ if (summarymapConfigProducer != null) {
+ SummarymapConfig.Builder summarymapConfigBuilder = new SummarymapConfig.Builder();
+ summarymapConfigProducer.getConfig(summarymapConfigBuilder);
+ summarymapConfig = new SummarymapConfig(summarymapConfigBuilder);
+ }
+
+ for (SummaryConfig.Classes sclass : summaryConfig.classes()) {
+ DocumentdbInfoConfig.Documentdb.Summaryclass.Builder sumClassBuilder = new DocumentdbInfoConfig.Documentdb.Summaryclass.Builder();
+ sumClassBuilder.
+ id(sclass.id()).
+ name(sclass.name());
+ for (SummaryConfig.Classes.Fields field : sclass.fields()) {
+ DocumentdbInfoConfig.Documentdb.Summaryclass.Fields.Builder fieldsBuilder = new DocumentdbInfoConfig.Documentdb.Summaryclass.Fields.Builder();
+ fieldsBuilder.name(field.name())
+ .type(field.type())
+ .dynamic(isDynamic(field.name(), summarymapConfig));
+ sumClassBuilder.fields(fieldsBuilder);
+ }
+ docDb.summaryclass(sumClassBuilder);
+ }
+ }
+
+ /** Returns true if this is a dynamic summary field */
+ private boolean isDynamic(String fieldName, SummarymapConfig summarymapConfig) {
+ if (summarymapConfig == null) return false; // not know for streaming, but also not used
+
+ for (SummarymapConfig.Override override : summarymapConfig.override()) {
+ if ( ! fieldName.equals(override.field())) continue;
+ if (SummaryMap.isDynamicCommand(override.command())) return true;
+ }
+ return false;
+ }
+
+ protected void addRankProfilesConfig(DocumentdbInfoConfig.Documentdb.Builder docDbBuilder, RankProfilesConfig rankProfilesCfg) {
+ for (RankProfilesConfig.Rankprofile rankProfile : rankProfilesCfg.rankprofile()) {
+ DocumentdbInfoConfig.Documentdb.Rankprofile.Builder rpB = new DocumentdbInfoConfig.Documentdb.Rankprofile.Builder();
+ rpB.name(rankProfile.name());
+ rpB.hasSummaryFeatures(containsPropertiesWithPrefix(RawRankProfile.summaryFeatureFefPropertyPrefix, rankProfile.fef()));
+ rpB.hasRankFeatures(containsPropertiesWithPrefix(RawRankProfile.rankFeatureFefPropertyPrefix, rankProfile.fef()));
+ docDbBuilder.rankprofile(rpB);
+ }
+ }
+
+ private boolean containsPropertiesWithPrefix(String prefix, RankProfilesConfig.Rankprofile.Fef fef) {
+ for (RankProfilesConfig.Rankprofile.Fef.Property p : fef.property()) {
+ if (p.name().startsWith(prefix))
+ return true;
+ }
+ return false;
+ }
+
+ protected abstract void deriveAllSearchDefinitions(List<SearchDefinitionSpec> localSearches,
+ List<com.yahoo.searchdefinition.Search> globalSearches);
+
+ public abstract void defaultDocumentsConfig();
+ public abstract DerivedConfiguration getSdConfig();
+ protected abstract void exportSdFiles(File toDir) throws IOException;
+ protected final void writeSdFiles(File toDir) throws IOException {
+ assureSdConsistent();
+ exportSdFiles(toDir);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/SearchColumn.java b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchColumn.java
new file mode 100644
index 00000000000..adeee257317
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchColumn.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SearchColumn extends AbstractConfigProducer {
+
+ // All search nodes contained in this column, these also exist as child config producers.
+ private final List<SearchNode> nodes = new LinkedList<>();
+
+ public SearchColumn(SearchCluster parent, String name, int index) {
+ super(parent, name);
+ }
+
+ /** @return The number of rows in this column. */
+ public int getNumRows() { return nodes.size(); }
+
+ /** @return All search nodes contained in this column. */
+ public List<SearchNode> getSearchNodes() { return nodes; }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinition.java b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinition.java
new file mode 100644
index 00000000000..2904c561d71
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinition.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.searchdefinition.Search;
+
+import java.util.Collection;
+
+/**
+ * @author tonytv
+ */
+public class SearchDefinition {
+
+ private final Search search;
+ private final String name;
+
+ public static final String fileNameSuffix = ".sd";
+
+ public Search getSearch() {
+ return search;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public SearchDefinition(String name, Search search) {
+ this.name = name;
+ this.search = search;
+ }
+
+ //Find search definition from a collection with the name specified
+ public static SearchDefinition findByName(final String searchDefinitionName, Collection<SearchDefinition> searchDefinitions) {
+ for (SearchDefinition candidate : searchDefinitions) {
+ if (candidate.getName().equals(searchDefinitionName) )
+ return candidate;
+ }
+
+ return null;
+ }
+
+ // Used by admin interface
+ public String getFilename() {
+ return getName() + fileNameSuffix;
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinitionXMLHandler.java b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinitionXMLHandler.java
new file mode 100644
index 00000000000..5ac43b01a5f
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchDefinitionXMLHandler.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import org.w3c.dom.Element;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Represents a single searchdefinition file.
+ *
+ * @author arnej27959
+ */
+public class SearchDefinitionXMLHandler implements Serializable {
+
+ private String sdName;
+
+ public SearchDefinitionXMLHandler(ModelElement elem) {
+ sdName = elem.getStringAttribute("name");
+ if (sdName == null) {
+ sdName = elem.getStringAttribute("type");
+ }
+ }
+
+ public String getName() { return sdName; }
+
+ public SearchDefinition getResponsibleSearchDefinition(List<SearchDefinition> searchDefinitions) {
+ return SearchDefinition.findByName( getName(), searchDefinitions );
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/SearchInterface.java b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchInterface.java
new file mode 100644
index 00000000000..bedc3684ea9
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchInterface.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+/**
+ * This represents an interface for searching.
+ * It can be both a backend search node or a dispatcher.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public interface SearchInterface {
+
+ NodeSpec getNodeSpec();
+ String getDispatcherConnectSpec();
+ String getHostName();
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/SearchNode.java b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchNode.java
new file mode 100644
index 00000000000..9bc968bfd1a
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchNode.java
@@ -0,0 +1,288 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.metrics.MetricsmanagerConfig;
+import com.yahoo.searchlib.TranslogserverConfig;
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.vespa.config.content.StorFilestorConfig;
+import com.yahoo.vespa.config.content.core.StorBucketmoverConfig;
+import com.yahoo.vespa.config.content.core.StorCommunicationmanagerConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.config.content.core.StorStatusConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.config.storage.StorDevicesConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.admin.MonitoringSystem;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import com.yahoo.vespa.model.content.ContentNode;
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+/**
+ * Represents a search node (proton).
+ * <p>
+ * Due to the current disconnect between StorageNode and SearchNode, we have to
+ * duplicate the set of RestartConfigs classes from StorageNode here, as SearchNode
+ * runs in a content/storage node context without this being immediately obvious
+ * in the model.
+ *
+ * @author arnej27959
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ */
+@RestartConfigs({ProtonConfig.class, MetricsmanagerConfig.class, TranslogserverConfig.class,
+ StorDevicesConfig.class, StorFilestorConfig.class, StorBucketmoverConfig.class,
+ StorCommunicationmanagerConfig.class, StorStatusConfig.class,
+ StorServerConfig.class, LoadTypeConfig.class})
+public class SearchNode extends AbstractService implements
+ SearchInterface,
+ ProtonConfig.Producer,
+ MetricsmanagerConfig.Producer,
+ TranslogserverConfig.Producer {
+
+ private static final long serialVersionUID = 1L;
+ private final boolean flushOnShutdown;
+ private NodeSpec nodeSpec;
+ private int distributionKey;
+ private final String clusterName;
+ private TransactionLogServer tls;
+ private AbstractService serviceLayerService;
+
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<SearchNode> {
+
+ private final String name;
+ private final NodeSpec nodeSpec;
+ private final String clusterName;
+ private final ContentNode contentNode;
+ private final boolean flushOnShutdown;
+ public Builder(String name, NodeSpec nodeSpec, String clusterName, ContentNode node, boolean flushOnShutdown) {
+ this.name = name;
+ this.nodeSpec = nodeSpec;
+ this.clusterName = clusterName;
+ this.contentNode = node;
+ this.flushOnShutdown = flushOnShutdown;
+ }
+
+ @Override
+ protected SearchNode doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ return new SearchNode(ancestor, name, contentNode.getDistributionKey(), nodeSpec, clusterName, contentNode, flushOnShutdown);
+ }
+ }
+
+ /**
+ * Creates a SearchNode in elastic mode.
+ */
+ public static SearchNode create(AbstractConfigProducer parent, String name, int distributionKey, NodeSpec nodeSpec,
+ String clusterName, AbstractService serviceLayerService, boolean flushOnShutdown) {
+ return new SearchNode(parent, name, distributionKey, nodeSpec, clusterName, serviceLayerService, flushOnShutdown);
+ }
+
+ private SearchNode(AbstractConfigProducer parent, String name, int distributionKey, NodeSpec nodeSpec,
+ String clusterName, AbstractService serviceLayerService, boolean flushOnShutdown) {
+ this(parent, name, nodeSpec, clusterName, flushOnShutdown);
+ this.distributionKey = distributionKey;
+ this.serviceLayerService = serviceLayerService;
+ setPropertiesElastic(clusterName, distributionKey);
+ }
+
+ private SearchNode(AbstractConfigProducer parent, String name, NodeSpec nodeSpec, String clusterName, boolean flushOnShutdown) {
+ super(parent, name);
+ this.nodeSpec = nodeSpec;
+ this.clusterName = clusterName;
+ this.flushOnShutdown = flushOnShutdown;
+ portsMeta.on(0).tag("rpc").tag("rtc").tag("admin").tag("status");
+ portsMeta.on(1).tag("fs4");
+ portsMeta.on(2).tag("srmp").tag("hack").tag("test");
+ portsMeta.on(3).tag("rpc").tag("engines-provider");
+ portsMeta.on(4).tag("http").tag("json").tag("health").tag("state");
+ // Properties are set in DomSearchBuilder
+ monitorService();
+ }
+
+ private void setPropertiesElastic(String clusterName, int distributionKey) {
+ setProp("index", distributionKey).
+ setProp("clustertype", "search").
+ setProp("clustername", clusterName);
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ private String getClusterConfigId() {
+ return getParent().getConfigId();
+ }
+
+ private String getBaseDir() {
+ return Defaults.getDefaults().vespaHome() + "var/db/vespa/search/cluster." + getClusterName() + "/n" + distributionKey;
+ }
+
+ public void updatePartition(int partitionId) {
+ nodeSpec = new NodeSpec(nodeSpec.rowId(), partitionId);
+ }
+
+ @Override
+ public NodeSpec getNodeSpec() {
+ return nodeSpec;
+ }
+
+ /**
+ * Returns the connection spec string that resolves to this search node.
+ *
+ * @return The connection string.
+ */
+ public String getConnectSpec() {
+ return "tcp/" + getHost().getHostName() + ":" + getRpcPort();
+ }
+
+ /**
+ * Returns the number of ports needed by this service.
+ *
+ * @return The number of ports.
+ */
+ @Override
+ public int getPortCount() {
+ return 5;
+ }
+
+ /**
+ * Returns the RPC port used by this searchnode.
+ *
+ * @return The port.
+ */
+ public int getRpcPort() {
+ return getRelativePort(0);
+ }
+
+ protected int getSlimeMessagingPort() {
+ return getRelativePort(2);
+ }
+
+ /*
+ * Returns the rpc port used for the engines provider interface.
+ * @return The port
+ */
+
+ public int getPersistenceProviderRpcPort() {
+ return getRelativePort(3);
+ }
+
+ @Override
+ public int getHealthPort() {
+ return getHttpPort();
+ }
+
+ @Override
+ public String getServiceType() {
+ return "searchnode";
+ }
+
+ public int getDistributionKey() {
+ return distributionKey;
+ }
+
+ /**
+ * Returns the connection spec string that resolves to the dispatcher service
+ * on this node.
+ *
+ * @return The connection string.
+ */
+ public String getDispatcherConnectSpec() {
+ return "tcp/" + getHost().getHostName() + ":" + getDispatchPort();
+ }
+
+ public int getDispatchPort() {
+ return getRelativePort(1);
+ }
+
+ public int getHttpPort() {
+ return getRelativePort(4);
+ }
+
+ @Override
+ public void getConfig(TranslogserverConfig.Builder builder) {
+ tls.getConfig(builder);
+ }
+
+ @Override
+ public String toString() {
+ return getHostName();
+ }
+
+ public TransactionLogServer getTransactionLogServer() {
+ return tls;
+ }
+
+ public void setTls(TransactionLogServer tls) {
+ this.tls = tls;
+ }
+
+ public AbstractService getServiceLayerService() {
+ return serviceLayerService;
+ }
+
+ @Override
+ public String getStartupCommand() {
+ String startup = getMMapNoCoreEnvVariable() + "exec $ROOT/sbin/proton " + "--identity " + getConfigId();
+ if (serviceLayerService != null) {
+ startup = startup + " --serviceidentity " + serviceLayerService.getConfigId();
+ }
+ return startup;
+ }
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ builder.
+ ptport(getDispatchPort()).
+ rpcport(getRpcPort()).
+ slime_messaging_port(getSlimeMessagingPort()).
+ rtcspec(getConnectSpec()).
+ httpport(getHttpPort()).
+ partition(getNodeSpec().partitionId()).
+ persistenceprovider(new ProtonConfig.Persistenceprovider.Builder().port(getPersistenceProviderRpcPort())).
+ clustername(getClusterName()).
+ basedir(getBaseDir()).
+ tlsspec("tcp/" + getHost().getHostName() + ":" + getTransactionLogServer().getTlsPort()).
+ tlsconfigid(getConfigId()).
+ slobrokconfigid(getClusterConfigId()).
+ routingconfigid(getClusterConfigId()).
+ distributionkey(getDistributionKey());
+ if (isHostedVespa()) {
+ // 4 days, 1 hour, 1 minute due to failed nodes can be in failed for 4 days and we want at least one hour more
+ // to make sure the node failer has done its work
+ builder.pruneremoveddocumentsage(4 * 24 * 3600 + 3600 + 60);
+ }
+ }
+
+ @Override
+ public HashMap<String, String> getDefaultMetricDimensions() {
+ HashMap<String, String> dimensions = new HashMap<>();
+ if (clusterName != null) {
+ dimensions.put("clustername", clusterName);
+ }
+ return dimensions;
+ }
+
+ @Override
+ public void getConfig(MetricsmanagerConfig.Builder builder) {
+ MonitoringSystem point = getMonitoringService();
+ if (point != null) {
+ builder.snapshot(new MetricsmanagerConfig.Snapshot.Builder().
+ periods(point.getIntervalSeconds()).periods(300));
+ }
+ builder.consumer(
+ new MetricsmanagerConfig.Consumer.Builder().
+ name("log").
+ tags("logdefault"));
+ }
+
+ @Override
+ public Optional<String> getPreShutdownCommand() {
+ return Optional.ofNullable(flushOnShutdown ? Defaults.getDefaults().vespaHome() + "bin/vespa-proton-cmd " + getRpcPort() + " prepareRestart" : null);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/SearchNodeWrapper.java b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchNodeWrapper.java
new file mode 100644
index 00000000000..caa918aab97
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/SearchNodeWrapper.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+public class SearchNodeWrapper implements SearchInterface {
+
+ private final NodeSpec nodeSpec;
+ private final SearchNode node;
+
+ public SearchNodeWrapper(NodeSpec nodeSpec, SearchNode node) {
+ this.nodeSpec = nodeSpec;
+ this.node = node;
+ }
+
+ @Override
+ public NodeSpec getNodeSpec() {
+ return nodeSpec;
+ }
+
+ @Override
+ public String getDispatcherConnectSpec() {
+ return node.getDispatcherConnectSpec();
+ }
+
+ @Override
+ public String getHostName() {
+ return node.getHostName();
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/StreamingSearchCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/search/StreamingSearchCluster.java
new file mode 100644
index 00000000000..2d89eb59b58
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/StreamingSearchCluster.java
@@ -0,0 +1,123 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.vespa.config.search.SummaryConfig;
+import com.yahoo.vespa.config.search.SummarymapConfig;
+import com.yahoo.vespa.config.search.vsm.VsmfieldsConfig;
+import com.yahoo.vespa.config.search.vsm.VsmsummaryConfig;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A search cluster of type streaming.
+ *
+ * @author balder
+ * @author vegardh
+ */
+public class StreamingSearchCluster extends SearchCluster implements
+ DocumentdbInfoConfig.Producer,
+ RankProfilesConfig.Producer,
+ VsmsummaryConfig.Producer,
+ VsmfieldsConfig.Producer,
+ SummarymapConfig.Producer,
+ SummaryConfig.Producer
+{
+
+ private final String storageRouteSpec;
+ private DerivedConfiguration sdConfig = null;
+
+ public StreamingSearchCluster(AbstractConfigProducer parent, String clusterName, int index, String storageClusterName, String storageRouteSpec) {
+ super(parent, clusterName, index);
+ this.storageRouteSpec = storageRouteSpec;
+ }
+
+ @Override
+ protected IndexingMode getIndexingMode() { return IndexingMode.STREAMING; }
+ public final String getStorageRouteSpec() { return storageRouteSpec; }
+ public int getRowBits() { return 0; }
+
+ @Override
+ public void getConfig(DocumentdbInfoConfig.Builder builder) {
+ DocumentdbInfoConfig.Documentdb.Builder docDb = new DocumentdbInfoConfig.Documentdb.Builder();
+ String searchName = sdConfig.getSearch().getName();
+ docDb.name(searchName);
+ SummaryConfig.Producer prod = sdConfig.getSummaries();
+ convertSummaryConfig(prod, null, docDb);
+ RankProfilesConfig.Builder rpb = new RankProfilesConfig.Builder();
+ sdConfig.getRankProfileList().getConfig(rpb);
+ addRankProfilesConfig(docDb, new RankProfilesConfig(rpb));
+ builder.documentdb(docDb);
+ }
+
+ @Override
+ protected void assureSdConsistent() {
+ if (sdConfig == null) {
+ throw new IllegalStateException("Search cluster '" + getClusterName() + "' does not have any search definitions");
+ }
+ }
+
+ protected void deriveAllSearchDefinitions(List<SearchDefinitionSpec> local,
+ List<com.yahoo.searchdefinition.Search> global) {
+ if (local.size() == 1) {
+ deriveSingleSearchDefinition(local.get(0).getSearchDefinition().getSearch(), global);
+ } else if (local.size() > 1){
+ throw new IllegalStateException("Logical indexes are not supported: Got " + local.size() + " search definitions, expected 1");
+ }
+ }
+ private void deriveSingleSearchDefinition(com.yahoo.searchdefinition.Search localSearch,
+ List<com.yahoo.searchdefinition.Search> globalSearches) {
+ this.sdConfig = new DerivedConfiguration(localSearch, globalSearches, deployLogger(), getRoot().getDeployState().rankProfileRegistry());
+ }
+ @Override
+ public DerivedConfiguration getSdConfig() {
+ return sdConfig;
+ }
+ @Override
+ protected void exportSdFiles(File toDir) throws IOException {
+ if (sdConfig!=null) {
+ sdConfig.export(toDir.getCanonicalPath());
+ }
+ }
+ @Override
+ public void defaultDocumentsConfig() { }
+
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ if (getSdConfig()!=null) getSdConfig().getAttributeFields().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(VsmsummaryConfig.Builder builder) {
+ if (getSdConfig()!=null)
+ if (getSdConfig().getVsmSummary()!=null)
+ getSdConfig().getVsmSummary().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(VsmfieldsConfig.Builder builder) {
+ if (getSdConfig()!=null)
+ if (getSdConfig().getVsmFields()!=null)
+ getSdConfig().getVsmFields().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(SummarymapConfig.Builder builder) {
+ if (getSdConfig()!=null)
+ if (getSdConfig().getSummaryMap()!=null)
+ getSdConfig().getSummaryMap().getConfig(builder);
+ }
+
+ @Override
+ public void getConfig(SummaryConfig.Builder builder) {
+ if (getSdConfig()!=null)
+ if (getSdConfig().getSummaries()!=null)
+ getSdConfig().getSummaries().getConfig(builder);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/TransactionLogServer.java b/config-model/src/main/java/com/yahoo/vespa/model/search/TransactionLogServer.java
new file mode 100644
index 00000000000..08c9d0ff4ce
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/TransactionLogServer.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.searchlib.TranslogserverConfig;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import org.w3c.dom.Element;
+
+/**
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class TransactionLogServer extends AbstractService {
+
+ private static final long serialVersionUID = 1L;
+
+ public TransactionLogServer(AbstractConfigProducer searchNode, String clusterName) {
+ super(searchNode, "transactionlogserver");
+ portsMeta.on(0).tag("tls");
+ setProp("clustername", clusterName);
+ setProp("clustertype", "search");
+ }
+
+ public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<TransactionLogServer> {
+ private final String clusterName;
+ public Builder(String clusterName) {
+ this.clusterName = clusterName;
+ }
+
+ @Override
+ protected TransactionLogServer doBuild(AbstractConfigProducer ancestor, Element producerSpec) {
+ return new TransactionLogServer(ancestor, clusterName);
+ }
+ }
+
+ public int getPortCount() {
+ return 1;
+ }
+
+ /**
+ * Returns the port used by the TLS.
+ *
+ * @return The port.
+ */
+ public int getTlsPort() {
+ return getRelativePort(0);
+ }
+
+ /**
+ * Returns the directory used by the TLS.
+ *
+ * @return The directory.
+ */
+ private String getTlsDir() {
+ return "tls";
+ }
+
+ public void getConfig(TranslogserverConfig.Builder builder) {
+ builder.listenport(getTlsPort()).basedir(getTlsDir());
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/search/Tuning.java b/config-model/src/main/java/com/yahoo/vespa/model/search/Tuning.java
new file mode 100644
index 00000000000..8493f5414be
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/search/Tuning.java
@@ -0,0 +1,377 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.model.content.TuningDispatch;
+
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+/**
+ * Class representing the tuning config used for a search cluster.
+ * Take a look at proton.def and vespa doc for detailed explanations.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class Tuning extends AbstractConfigProducer implements PartitionsConfig.Producer, ProtonConfig.Producer {
+
+ public static class Dispatch implements PartitionsConfig.Producer {
+
+ public Integer maxHitsPerPartition = null;
+ public TuningDispatch.DispatchPolicy policy = TuningDispatch.DispatchPolicy.ROUNDROBIN;
+ public boolean useLocalNode = false;
+ public Double minGroupCoverage = null;
+ public Double minActiveDocsCoverage = null;
+
+ @Override
+ public void getConfig(PartitionsConfig.Builder builder) {
+ if (maxHitsPerPartition != null) {
+ for (PartitionsConfig.Dataset.Builder dataset : builder.dataset) {
+ dataset.maxhitspernode(maxHitsPerPartition);
+ }
+ }
+ if (minGroupCoverage != null) {
+ for (PartitionsConfig.Dataset.Builder dataset : builder.dataset) {
+ dataset.min_group_coverage(minGroupCoverage);
+ }
+ }
+ if (minActiveDocsCoverage != null) {
+ for (PartitionsConfig.Dataset.Builder dataset : builder.dataset) {
+ dataset.min_activedocs_coverage(minActiveDocsCoverage);
+ }
+ }
+ for (PartitionsConfig.Dataset.Builder dataset : builder.dataset) {
+ switch (policy) {
+ case RANDOM:
+ dataset.useroundrobinforfixedrow(false);
+ break;
+ case ROUNDROBIN:
+ default:
+ dataset.useroundrobinforfixedrow(true);
+ break;
+ }
+ }
+ }
+ }
+
+ public static class SearchNode implements ProtonConfig.Producer {
+
+ public enum IoType {
+ NORMAL("NORMAL"),
+ DIRECTIO("DIRECTIO"),
+ MMAP("MMAP");
+
+ public final String name;
+
+ IoType(String name) {
+ this.name = name;
+ }
+
+ public static IoType fromString(String name) {
+ for (IoType type : IoType.values()) {
+ if (toLowerCase(name).equals(toLowerCase(type.name))) {
+ return type;
+ }
+ }
+ return NORMAL;
+ }
+ }
+
+ public static class RequestThreads implements ProtonConfig.Producer {
+ public Integer numSearchThreads = null;
+ public Integer numThreadsPerSearch = null;
+ public Integer numSummaryThreads = null;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ if (numSearchThreads!=null) builder.numsearcherthreads(numSearchThreads);
+ if (numThreadsPerSearch!=null) builder.numthreadspersearch(numThreadsPerSearch);
+ if (numSummaryThreads!=null) builder.numsummarythreads(numSummaryThreads);
+ }
+ }
+
+ public static class FlushStrategy implements ProtonConfig.Producer {
+ public Long totalMaxMemoryGain = null;
+ public Double totalDiskBloatFactor = null;
+ public Long componentMaxMemoryGain = null;
+ public Double componentDiskBloatFactor = null;
+ public Double componentMaxage = null;
+ public Long transactionLogMaxEntries = null;
+ public Long transactionLogMaxSize = null;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ // Here, the config building gets very ugly, because we have to check for null because of autoconversion Long/long etc.
+
+ ProtonConfig.Flush.Memory.Builder memoryBuilder = new ProtonConfig.Flush.Memory.Builder();
+ if (totalMaxMemoryGain != null) memoryBuilder.maxmemory(totalMaxMemoryGain);
+ if (totalDiskBloatFactor != null) memoryBuilder.diskbloatfactor(totalDiskBloatFactor);
+ if (transactionLogMaxSize != null) memoryBuilder.maxtlssize(transactionLogMaxSize);
+
+ ProtonConfig.Flush.Memory.Each.Builder eachBuilder = new ProtonConfig.Flush.Memory.Each.Builder();
+ if (componentMaxMemoryGain != null) eachBuilder.maxmemory(componentMaxMemoryGain);
+ if (componentDiskBloatFactor != null) eachBuilder.diskbloatfactor(componentDiskBloatFactor);
+ memoryBuilder.each(eachBuilder);
+
+ ProtonConfig.Flush.Memory.Maxage.Builder maxageBuilder = new ProtonConfig.Flush.Memory.Maxage.Builder();
+ if (componentMaxage != null) maxageBuilder.time(componentMaxage);
+ if (transactionLogMaxEntries != null) maxageBuilder.serial(transactionLogMaxEntries);
+ memoryBuilder.maxage(maxageBuilder);
+
+ builder.
+ flush(new ProtonConfig.Flush.Builder().
+ memory(memoryBuilder));
+ }
+ }
+
+ public static class Resizing implements ProtonConfig.Producer {
+ public Integer initialDocumentCount = null;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ if (initialDocumentCount!=null) builder.
+ grow(new ProtonConfig.Grow.Builder().
+ initial(initialDocumentCount));
+ }
+
+ }
+
+ public static class Index implements ProtonConfig.Producer {
+ public static class Io implements ProtonConfig.Producer {
+ public IoType write = null;
+ public IoType read = null;
+ public IoType search = null;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ ProtonConfig.Indexing.Builder indexingB = new ProtonConfig.Indexing.Builder();
+ if (write != null) indexingB.
+ write(new ProtonConfig.Indexing.Write.Builder().
+ io(ProtonConfig.Indexing.Write.Io.Enum.valueOf(write.name)));
+ if (read != null) indexingB.
+ read(new ProtonConfig.Indexing.Read.Builder().
+ io(ProtonConfig.Indexing.Read.Io.Enum.valueOf(read.name)));
+ if (search != null) builder.search(new ProtonConfig.Search.Builder().
+ io(ProtonConfig.Search.Io.Enum.valueOf(search.name)));
+ builder.indexing(indexingB);
+ }
+
+ }
+
+ public Io io;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ if (io != null) io.getConfig(builder);
+ }
+ }
+
+ public static class Attribute implements ProtonConfig.Producer {
+ public static class Io implements ProtonConfig.Producer {
+ public IoType write = null;
+
+ public Io() {}
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ if (write != null) builder.attribute(new ProtonConfig.Attribute.Builder().
+ write(new ProtonConfig.Attribute.Write.Builder().
+ io(ProtonConfig.Attribute.Write.Io.Enum.valueOf(write.name))));
+ }
+
+ }
+
+ public Io io;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ if (io != null) io.getConfig(builder);
+ }
+
+ }
+
+ public static class Summary implements ProtonConfig.Producer {
+ public static class Io {
+ public IoType write = null;
+ public IoType read = null;
+
+ public void getConfig(ProtonConfig.Summary.Builder builder) {
+ if (write != null) builder.
+ write(new ProtonConfig.Summary.Write.Builder().
+ io(ProtonConfig.Summary.Write.Io.Enum.valueOf(write.name)));
+ if (read != null) builder.
+ read(new ProtonConfig.Summary.Read.Builder().
+ io(ProtonConfig.Summary.Read.Io.Enum.valueOf(read.name)));
+ }
+ }
+
+ public static class Store {
+ public static class Compression {
+ public enum Type {
+ NONE("NONE"),
+ LZ4("LZ4");
+
+ public final String name;
+
+ Type(String name) {
+ this.name = name;
+ }
+ public static Type fromString(String name) {
+ for (Type type : Type.values()) {
+ if (toLowerCase(name).equals(toLowerCase(type.name))) {
+ return type;
+ }
+ }
+ return NONE;
+ }
+ }
+ public Type type = null;
+ public Integer level = null;
+
+ public void getConfig(ProtonConfig.Summary.Cache.Compression.Builder compression) {
+ if (type != null) compression.type(ProtonConfig.Summary.Cache.Compression.Type.Enum.valueOf(type.name));
+ if (level != null) compression.level(level);
+ }
+
+ public void getConfig(ProtonConfig.Summary.Log.Chunk.Compression.Builder compression) {
+ if (type != null) compression.type(ProtonConfig.Summary.Log.Chunk.Compression.Type.Enum.valueOf(type.name));
+ if (level != null) compression.level(level);
+ }
+ }
+
+ public static class Component {
+ public Long maxSize = null;
+ public Long maxEntries = null;
+ public Long initialEntries = null;
+ public Compression compression = null;
+ private final boolean outputInt;
+
+ public Component() {
+ this.outputInt = false;
+ }
+
+ public Component(boolean outputInt) {
+ this.outputInt = outputInt;
+ }
+
+ public void getConfig(ProtonConfig.Summary.Cache.Builder cache) {
+ if (outputInt) {
+ if (maxSize!=null) cache.maxbytes(maxSize.intValue());
+ if (maxEntries!=null) cache.initialentries(maxEntries.intValue());
+ if (initialEntries!=null) cache.initialentries(initialEntries.intValue());
+ } else {
+ if (maxSize!=null) cache.maxbytes(maxSize);
+ if (maxEntries!=null) cache.initialentries(maxEntries);
+ if (initialEntries!=null) cache.initialentries(initialEntries);
+ }
+ if (compression != null) {
+ ProtonConfig.Summary.Cache.Compression.Builder compressionB = new ProtonConfig.Summary.Cache.Compression.Builder();
+ compression.getConfig(compressionB);
+ cache.compression(compressionB);
+ }
+ }
+
+ public void getConfig(ProtonConfig.Summary.Log.Chunk.Builder chunk) {
+ if (outputInt) {
+ if (maxSize!=null) chunk.maxbytes(maxSize.intValue());
+ if (maxEntries!=null) chunk.maxentries(maxEntries.intValue());
+ } else {
+ throw new IllegalStateException("Fix this, chunk does not have long types");
+ }
+ if (compression != null) {
+ ProtonConfig.Summary.Log.Chunk.Compression.Builder compressionB = new ProtonConfig.Summary.Log.Chunk.Compression.Builder();
+ compression.getConfig(compressionB);
+ chunk.compression(compressionB);
+ }
+ }
+ }
+
+ public static class LogStore {
+ public Long maxFileSize = null;
+ public Double maxDiskBloatFactor = null;
+ public Integer numThreads = null;
+ public Component chunk = null;
+ public Double minFileSizeFactor = null;
+
+ public void getConfig(ProtonConfig.Summary.Log.Builder log) {
+ if (maxFileSize!=null) log.maxfilesize(maxFileSize);
+ if (maxDiskBloatFactor!=null) log.maxdiskbloatfactor(maxDiskBloatFactor);
+ if (minFileSizeFactor!=null) log.minfilesizefactor(minFileSizeFactor);
+ if (numThreads != null) log.numthreads(numThreads);
+ if (chunk != null) {
+ ProtonConfig.Summary.Log.Chunk.Builder chunkB = new ProtonConfig.Summary.Log.Chunk.Builder();
+ chunk.getConfig(chunkB);
+ log.chunk(chunkB);
+ }
+ }
+ }
+
+ public Component cache;
+ public LogStore logStore;
+
+ public void getConfig(ProtonConfig.Summary.Builder builder) {
+ if (cache != null) {
+ ProtonConfig.Summary.Cache.Builder cacheB=new ProtonConfig.Summary.Cache.Builder();
+ cache.getConfig(cacheB);
+ builder.cache(cacheB);
+
+ }
+ if (logStore != null) {
+ ProtonConfig.Summary.Log.Builder logB = new ProtonConfig.Summary.Log.Builder();
+ logStore.getConfig(logB);
+ builder.log(logB);
+ }
+ }
+ }
+
+ public Io io;
+ public Store store;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ ProtonConfig.Summary.Builder summary = new ProtonConfig.Summary.Builder();
+ if (io != null) io.getConfig(summary);
+ if (store != null) store.getConfig(summary);
+ builder.summary(summary);
+ }
+ }
+
+ public RequestThreads threads = null;
+ public FlushStrategy strategy = null;
+ public Resizing resizing = null;
+ public Index index = null;
+ public Attribute attribute = null;
+ public Summary summary = null;
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ if (threads != null) threads.getConfig(builder);
+ if (strategy != null) strategy.getConfig(builder);
+ if (resizing != null) resizing.getConfig(builder);
+ if (index != null) index.getConfig(builder);
+ if (attribute != null) attribute.getConfig(builder);
+ if (summary != null) summary.getConfig(builder);
+ }
+ }
+
+ public Dispatch dispatch;
+ public SearchNode searchNode;
+
+ public Tuning(AbstractConfigProducer parent) {
+ super(parent, "tuning");
+ }
+
+ @Override
+ public void getConfig(PartitionsConfig.Builder builder) {
+ if (dispatch != null) {
+ dispatch.getConfig(builder);
+ }
+ }
+
+ @Override
+ public void getConfig(ProtonConfig.Builder builder) {
+ if (searchNode != null) searchNode.getConfig(builder);
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/utils/Duration.java b/config-model/src/main/java/com/yahoo/vespa/model/utils/Duration.java
new file mode 100644
index 00000000000..1c51f021fef
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/utils/Duration.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses a string on the form:
+ *
+ * [numbers]\s*[unit]?
+ *
+ * Where numbers is a double, and unit is one of:
+ * d - days
+ * m - minutes
+ * s - seconds
+ * ms - milliseconds
+ *
+ * Default is seconds.
+ */
+public class Duration {
+ private static Pattern pattern = Pattern.compile("([0-9\\.]+)\\s*([a-z]+)?");
+ private static Map<String, Integer> unitMultiplier = new HashMap<>();
+ static {
+ unitMultiplier.put("s", 1000);
+ unitMultiplier.put("d", 1000 * 3600 * 24);
+ unitMultiplier.put("ms", 1);
+ unitMultiplier.put("m", 60 * 1000);
+ unitMultiplier.put("h", 60 * 60 * 1000);
+ }
+
+ long value;
+
+ public Duration(String value) {
+ Matcher matcher = pattern.matcher(value);
+
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Illegal duration format: " + value);
+ }
+
+ double num = Double.parseDouble(matcher.group(1));
+ String unit = matcher.group(2);
+
+ if (unit != null) {
+ Integer multiplier = unitMultiplier.get(unit);
+ if (multiplier == null) {
+ throw new IllegalArgumentException("Unknown time unit: " + unit + " in time value " + value);
+ }
+
+ num *= multiplier;
+ } else {
+ num *= 1000;
+ }
+
+ this.value = (long)num;
+ }
+
+ public double getSeconds() {
+ return value / 1000.0;
+ }
+
+ public long getMilliSeconds() {
+ return value;
+ }
+
+}
+
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/utils/FileSender.java b/config-model/src/main/java/com/yahoo/vespa/model/utils/FileSender.java
new file mode 100644
index 00000000000..12de5a3fbb0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/utils/FileSender.java
@@ -0,0 +1,149 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.utils;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.producer.UserConfigRepo;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.*;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.config.ConfigDefinition.DefaultValued;
+import com.yahoo.vespa.model.AbstractService;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * Utility methods for sending files to a collection of nodes.
+ *
+ * @author gjoranv
+ * @since 5.1.9
+ */
+public class FileSender implements Serializable {
+
+ /**
+ * Send the given file to all given services.
+ *
+ * @param relativePath The path to the file, relative to the app pkg.
+ * @param services The services to send the file to.
+ * @return The file reference that the file was given, never null.
+ * @throws IllegalStateException if services is empty.
+ */
+ public static FileReference sendFileToServices(String relativePath,
+ Collection<? extends AbstractService> services) {
+ if (services.isEmpty()) {
+ throw new IllegalStateException("'sendFileToServices called for empty services!" +
+ " - This should never happen!");
+ }
+ FileReference fileref = null;
+ for (AbstractService service : services) {
+ // The same reference will be returned from each call.
+ fileref = service.sendFile(relativePath);
+ }
+ return fileref;
+ }
+
+ /**
+ * Sends all user configured files for a producer to all given services.
+ */
+ public static <PRODUCER extends AbstractConfigProducer<?>>
+ void sendUserConfiguredFiles(PRODUCER producer, Collection<? extends AbstractService> services, DeployLogger logger) {
+ if (services.isEmpty())
+ return;
+
+ UserConfigRepo userConfigs = producer.getUserConfigs();
+ Map<String, FileReference> sentFiles = new HashMap<>();
+ for (ConfigDefinitionKey key : userConfigs.configsProduced()) {
+ ConfigPayloadBuilder builder = userConfigs.get(key);
+ try {
+ sendUserConfiguredFiles(builder, sentFiles, services, key, logger);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Unable to send files for " + key, e);
+ }
+ }
+ }
+
+ private static void sendUserConfiguredFiles(ConfigPayloadBuilder builder,
+ Map<String, FileReference> sentFiles,
+ Collection<? extends AbstractService> services,
+ ConfigDefinitionKey key, DeployLogger logger) {
+ ConfigDefinition configDefinition = builder.getConfigDefinition();
+ if (configDefinition == null) {
+ // TODO: throw new IllegalArgumentException("Not able to find config definition for " + builder);
+ logger.log(LogLevel.WARNING, "Not able to find config definition for " + key + ". Will not send files for this config");
+ return;
+ }
+ // Inspect fields at this level
+ sendEntries(builder, sentFiles, services, configDefinition.getFileDefs());
+ sendEntries(builder, sentFiles, services, configDefinition.getPathDefs());
+
+ // Inspect arrays
+ for (Map.Entry<String, ConfigDefinition.ArrayDef> entry : configDefinition.getArrayDefs().entrySet()) {
+ if (isFileOrPathArray(entry)) {
+ ConfigPayloadBuilder.Array array = builder.getArray(entry.getKey());
+ sendFileEntries(array.getElements(), sentFiles, services);
+ }
+ }
+ // Maps
+ for (Map.Entry<String, ConfigDefinition.LeafMapDef> entry : configDefinition.getLeafMapDefs().entrySet()) {
+ if (isFileOrPathMap(entry)) {
+ ConfigPayloadBuilder.MapBuilder map = builder.getMap(entry.getKey());
+ sendFileEntries(map.getElements(), sentFiles, services);
+ }
+ }
+
+ // Inspect inner fields
+ for (String name : configDefinition.getStructDefs().keySet()) {
+ sendUserConfiguredFiles(builder.getObject(name), sentFiles, services, key, logger);
+ }
+ for (String name : configDefinition.getInnerArrayDefs().keySet()) {
+ ConfigPayloadBuilder.Array array = builder.getArray(name);
+ for (ConfigPayloadBuilder element : array.getElements()) {
+ sendUserConfiguredFiles(element, sentFiles, services, key, logger);
+ }
+ }
+ for (String name : configDefinition.getStructMapDefs().keySet()) {
+ ConfigPayloadBuilder.MapBuilder map = builder.getMap(name);
+ for (ConfigPayloadBuilder element : map.getElements()) {
+ sendUserConfiguredFiles(element, sentFiles, services, key, logger);
+ }
+ }
+
+ }
+
+ private static boolean isFileOrPathMap(Map.Entry<String, ConfigDefinition.LeafMapDef> entry) {
+ String mapType = entry.getValue().getTypeSpec().getType();
+ return ("file".equals(mapType) || "path".equals(mapType));
+ }
+
+ private static boolean isFileOrPathArray(Map.Entry<String, ConfigDefinition.ArrayDef> entry) {
+ String arrayType = entry.getValue().getTypeSpec().getType();
+ return ("file".equals(arrayType) || "path".equals(arrayType));
+ }
+
+ private static void sendEntries(ConfigPayloadBuilder builder, Map<String, FileReference> sentFiles, Collection<? extends AbstractService> services, Map<String, ? extends DefaultValued<String>> entries) {
+ for (String name : entries.keySet()) {
+ ConfigPayloadBuilder fileEntry = builder.getObject(name);
+ if (fileEntry.getValue() == null) {
+ throw new IllegalArgumentException("Unable to send file for field '" + name + "'. Invalid config value " + fileEntry.getValue());
+ }
+ sendFileEntry(fileEntry, sentFiles, services);
+ }
+ }
+
+ private static void sendFileEntries(Collection<ConfigPayloadBuilder> builders, Map<String, FileReference> sentFiles, Collection<? extends AbstractService> services) {
+ for (ConfigPayloadBuilder builder : builders) {
+ sendFileEntry(builder, sentFiles, services);
+ }
+ }
+
+ private static void sendFileEntry(ConfigPayloadBuilder builder, Map<String, FileReference> sentFiles, Collection<? extends AbstractService> services) {
+ String path = builder.getValue();
+ FileReference reference = sentFiles.get(path);
+ if (reference == null) {
+ reference = sendFileToServices(path, services);
+ sentFiles.put(path, reference);
+ }
+ builder.setValue(reference.value());
+ }
+} \ No newline at end of file
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/utils/FreezableMap.java b/config-model/src/main/java/com/yahoo/vespa/model/utils/FreezableMap.java
new file mode 100644
index 00000000000..a05008cc9a0
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/utils/FreezableMap.java
@@ -0,0 +1,91 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.utils;
+
+import java.util.*;
+
+/**
+ * Delegates to a map that can be froozen.
+ * Not thread safe.
+ * @author tonytv
+ */
+public class FreezableMap<K, V> implements Map<K, V> {
+ private boolean frozen = false;
+ private Map<K, V> map;
+
+ //TODO: review the use of unchecked.
+ @SuppressWarnings("unchecked")
+ public FreezableMap(Class<LinkedHashMap> mapClass) {
+ try {
+ map = mapClass.newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int size() {
+ return map.size();
+ }
+
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ public boolean containsKey(Object o) {
+ return map.containsKey(o);
+ }
+
+ public boolean containsValue(Object o) {
+ return map.containsValue(o);
+ }
+
+ public V get(Object o) {
+ return map.get(o);
+ }
+
+ public V put(K key, V value) {
+ return map.put(key, value);
+ }
+
+ public V remove(Object o) {
+ return map.remove(o);
+ }
+
+ public void putAll(Map<? extends K, ? extends V> map) {
+ this.map.putAll(map);
+ }
+
+ public void clear() {
+ map.clear();
+ }
+
+ public Set<K> keySet() {
+ return map.keySet();
+ }
+
+ public Collection<V> values() {
+ return map.values();
+ }
+
+ public Set<Entry<K, V>> entrySet() {
+ return map.entrySet();
+ }
+
+ public boolean equals(Object o) {
+ return map.equals(o);
+ }
+
+ public int hashCode() {
+ return map.hashCode();
+ }
+
+ public void freeze() {
+ if (frozen)
+ throw new RuntimeException("The map has already been frozen.");
+ frozen = true;
+ map = Collections.unmodifiableMap(map);
+ }
+
+ public boolean isFrozen() {
+ return frozen;
+ }
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/utils/internal/ReflectionUtil.java b/config-model/src/main/java/com/yahoo/vespa/model/utils/internal/ReflectionUtil.java
new file mode 100644
index 00000000000..2bc7d2dfbe7
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/utils/internal/ReflectionUtil.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.utils.internal;
+
+import com.yahoo.config.ChangesRequiringRestart;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Utility class containing static methods for retrievinig information about the config producer tree.
+ *
+ * @author lulf
+ * @author bjorncs
+ * @since 5.1
+ */
+public final class ReflectionUtil {
+
+ private ReflectionUtil() {
+ }
+
+ /**
+ * Returns a set of all the configs produced by a given producer.
+ *
+ * @param iface The config producer or interface to check for producers.
+ * @param configId The config id to use when creating keys.
+ * @return A set of config keys.
+ */
+ public static Set<ConfigKey<?>> configsProducedByInterface(Class<?> iface, String configId) {
+ Set<ConfigKey<?>> ret = new LinkedHashSet<>();
+ if (isConcreteProducer(iface)) {
+ ret.add(createConfigKeyFromInstance(iface.getEnclosingClass(), configId));
+ }
+ for (Class<?> parentIface : iface.getInterfaces()) {
+ ret.addAll(configsProducedByInterface(parentIface, configId));
+ }
+ return ret;
+ }
+
+ /**
+ * Determines if the config class contains the methods required for detecting config value changes
+ * between two config instances.
+ */
+ public static boolean hasRestartMethods(Class<? extends ConfigInstance> configClass) {
+ try {
+ configClass.getDeclaredMethod("containsFieldsFlaggedWithRestart");
+ configClass.getDeclaredMethod("getChangesRequiringRestart", configClass);
+ return true;
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Determines if the config definition for the given config class contains key-values flagged with restart.
+ */
+ public static boolean containsFieldsFlaggedWithRestart(Class<? extends ConfigInstance> configClass) {
+ try {
+ Method m = configClass.getDeclaredMethod("containsFieldsFlaggedWithRestart");
+ m.setAccessible(true);
+ return (boolean) m.invoke(null);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Compares the config instances and lists any differences that will require service restart.
+ * @param from The previous config.
+ * @param to The new config.
+ * @return An object describing the difference.
+ */
+ public static ChangesRequiringRestart getChangesRequiringRestart(ConfigInstance from, ConfigInstance to) {
+ Class<?> clazz = from.getClass();
+ if (!clazz.equals(to.getClass())) {
+ throw new IllegalArgumentException(String.format("%s != %s", clazz, to.getClass()));
+ }
+ try {
+ Method m = clazz.getDeclaredMethod("getChangesRequiringRestart", clazz);
+ m.setAccessible(true);
+ return (ChangesRequiringRestart) m.invoke(from, to);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ConfigKey<?> createConfigKeyFromInstance(Class<?> configInstClass, String configId) {
+ try {
+ String defName = ConfigInstance.getDefName(configInstClass);
+ String defNamespace = ConfigInstance.getDefNamespace(configInstClass);
+ return new ConfigKey<>(defName, configId, defNamespace);
+ } catch (IllegalArgumentException | SecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean classIsConfigInstanceProducer(Class<?> clazz) {
+ return clazz.getName().equals(ConfigInstance.Producer.class.getName());
+ }
+
+ private static boolean isConcreteProducer(Class<?> producerInterface) {
+ boolean parentIsConfigInstance = false;
+ for (Class<?> ifaceParent : producerInterface.getInterfaces()) {
+ if (classIsConfigInstanceProducer(ifaceParent)) {
+ parentIsConfigInstance = true;
+ }
+ }
+ return (ConfigInstance.Producer.class.isAssignableFrom(producerInterface) && parentIsConfigInstance && !classIsConfigInstanceProducer(producerInterface));
+ }
+
+}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/utils/package-info.java b/config-model/src/main/java/com/yahoo/vespa/model/utils/package-info.java
new file mode 100644
index 00000000000..c19058af4fa
--- /dev/null
+++ b/config-model/src/main/java/com/yahoo/vespa/model/utils/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.model.utils;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/config-model/src/main/javacc/SDParser.jj b/config-model/src/main/javacc/SDParser.jj
new file mode 100644
index 00000000000..37ab65b7f77
--- /dev/null
+++ b/config-model/src/main/javacc/SDParser.jj
@@ -0,0 +1,2516 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// --------------------------------------------------------------------------------
+//
+// JavaCC options. When this file is changed, run "mvn generate-sources" to rebuild
+// the parser classes.
+//
+// --------------------------------------------------------------------------------
+options {
+ UNICODE_INPUT = true;
+ CACHE_TOKENS = false;
+ STATIC = false;
+ DEBUG_PARSER = false;
+ ERROR_REPORTING = true;
+ FORCE_LA_CHECK = true;
+ USER_CHAR_STREAM = true;
+}
+
+// --------------------------------------------------------------------------------
+//
+// Parser body.
+//
+// --------------------------------------------------------------------------------
+PARSER_BEGIN(SDParser)
+
+package com.yahoo.searchdefinition.parser;
+
+import com.yahoo.document.*;
+import com.yahoo.documentmodel.*;
+import com.yahoo.compress.Compressor;
+import com.yahoo.compress.CompressionType;
+import com.yahoo.searchdefinition.document.*;
+import com.yahoo.searchdefinition.document.annotation.SDAnnotationType;
+import com.yahoo.searchdefinition.document.annotation.TemporaryAnnotationReferenceDataType;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.DefaultRankProfile;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.RankProfile.MatchPhaseSettings;
+import com.yahoo.searchdefinition.RankProfile.DiversitySettings;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.UnproperSearch;
+import com.yahoo.searchdefinition.UnrankedRankProfile;
+import com.yahoo.searchdefinition.fieldoperation.*;
+import com.yahoo.searchlib.rankingexpression.FeatureList;
+import com.yahoo.searchlib.rankingexpression.evaluation.Value;
+import com.yahoo.searchlib.rankingexpression.evaluation.TensorValue;
+import com.yahoo.tensor.Tensor;
+import com.yahoo.tensor.TensorType;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.language.Linguistics;
+import com.yahoo.language.simple.SimpleLinguistics;
+import com.yahoo.search.query.ranking.Diversity;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.logging.Level;
+
+/**
+ * A search definition parser
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a> et.al.
+ */
+public class SDParser {
+ private DocumentTypeManager docMan = null;
+ private ApplicationPackage app = MockApplicationPackage.createEmpty();
+ private DeployLogger deployLogger = new BaseDeployLogger();
+ private RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+
+ public SDParser(String input, DeployLogger deployLogger) {
+ this(new SimpleCharStream(input), deployLogger);
+ }
+
+ public SDParser(SimpleCharStream stream, DeployLogger deployLogger) {
+ this(stream);
+ this.deployLogger = deployLogger;
+ }
+
+ public SDParser(SimpleCharStream stream, DeployLogger deployLogger, ApplicationPackage applicationPackage, RankProfileRegistry rankProfileRegistry) {
+ this(stream);
+ this.deployLogger = deployLogger;
+ this.app = applicationPackage;
+ this.rankProfileRegistry = rankProfileRegistry;
+ }
+
+ /**
+ * Consumes an indexing language script which will use the simple linguistics implementation
+ * for testing, by taking input from the current input stream.
+ *
+ * @param multiline Whether or not to allow multi-line expressions.
+ */
+ private IndexingOperation newIndexingOperation(boolean multiline) throws ParseException {
+ return newIndexingOperation(multiline, new SimpleLinguistics());
+ }
+
+ /**
+ * Consumes an indexing language script from the current input stream.
+ *
+ * @param multiline Whether or not to allow multi-line expressions.
+ * @param linguistics What to use for tokenizing.
+ */
+ private IndexingOperation newIndexingOperation(boolean multiline, Linguistics linguistics) throws ParseException {
+ SimpleCharStream input = (SimpleCharStream)token_source.input_stream;
+ if (token.next != null) {
+ input.backup(token.next.image.length());
+ }
+ try {
+ return IndexingOperation.fromStream(input, multiline, linguistics);
+ } finally {
+ token.next = null;
+ jj_ntk = -1;
+ }
+ }
+
+ /**
+ * Parses the given token image as a ranking expression feature list.
+ *
+ * @param image The token image to parse.
+ * @return The consumed feature list.
+ * @throws ParseException Thrown if the image could not be parsed.
+ */
+ private FeatureList getFeatureList(String image) throws ParseException {
+ try {
+ return new FeatureList(image);
+ }
+ catch (com.yahoo.searchlib.rankingexpression.parser.ParseException e) {
+ throw (ParseException) new ParseException("Could not parse feature list '" + image + "' at line " +
+ token_source.input_stream.getBeginLine() + ", column " +
+ token_source.input_stream.getBeginColumn() + ".").initCause(e);
+ }
+ }
+
+ /**
+ * Sets the compression threshold in each item in the compression config array.
+ *
+ * @param cfg The array of configs to modify.
+ * @param val The compression threshold to set.
+ */
+ private void setCompressionThreshold(CompressionConfig cfg, int val) {
+ cfg.threshold = val;
+ }
+
+ /**
+ * Sets the compression level in each item in the compression config array.
+ *
+ * @param cfg The array of configs to modify.
+ * @param val The compression level to set.
+ */
+ private void setCompressionLevel(CompressionConfig cfg, int val) {
+ cfg.compressionLevel = val;
+ }
+}
+
+PARSER_END(SDParser)
+
+
+// --------------------------------------------------------------------------------
+//
+// Token declarations.
+//
+// --------------------------------------------------------------------------------
+
+// Declare white space characters. These do not include newline because it has
+// special meaning in several of the production rules.
+SKIP :
+{
+ " " | "\t" | "\r" | "\f"
+}
+
+// Declare all tokens to be recognized. When a word token is added it MUST be
+// added to the identifier() production rule.
+TOKEN :
+{
+ < NL: "\n" >
+| < ANNOTATION: "annotation" >
+| < ANNOTATIONREFERENCE: "annotationreference" >
+| < SEARCH: "search" >
+| < DIVERSITY: "diversity" >
+| < MIN_GROUPS: "min-groups" >
+| < CUTOFF_FACTOR: "cutoff-factor" >
+| < CUTOFF_STRATEGY: "cutoff-strategy" >
+| < LOOSE: "loose" >
+| < STRICT: "strict" >
+| < DOCUMENT: "document" >
+| < STRUCT: "struct" >
+| < INHERITS: "inherits" >
+| < FIELD: "field" >
+| < FIELDS: "fields" >
+| < FIELDSET: "fieldset" >
+| < STRUCTFIELD: "struct-field" >
+| < INDEXING: "indexing" >
+| < SUMMARYTO: "summary-to" >
+| < DOCUMENTSUMMARY: "document-summary" >
+| < RANKTYPE: "rank-type" >
+| < WEIGHT: "weight" >
+| < TYPE: "type" >
+| < INDEX: "index" >
+| < RISE: "rise" >
+| < MTOKEN: "token" >
+| < TEXT: "text" >
+| < WORD: "word" >
+| < GRAM: "gram" >
+| < GRAMSIZE: "gram-size" >
+| < MAXLENGTH: "max-length" >
+| < PREFIX: "prefix" >
+| < SUBSTRING: "substring" >
+| < SUFFIX: "suffix" >
+| < RANKPROFILE: "rank-profile" >
+| < RANKDEGRADATIONFREQ: "rank-degradation-frequency" >
+| < RANKDEGRADATION: "rank-degradation" >
+| < RPBINSIZE: "doc-frequency" >
+| < RPBINLOW: "min-fullrank-docs">
+| < RPPOSBINSIZE: "occurrences-per-doc" >
+| < SUMMARY: "summary" >
+| < FULL: "full" >
+| < STATIC: "static" >
+| < DYNAMIC: "dynamic" >
+| < SSCONTEXTUAL: "contextual" >
+| < SSOVERRIDE: "override" >
+| < SSTITLE: "title" >
+| < SSURL: "url" >
+| < PROPERTIES: "properties" >
+| < ATTRIBUTE: "attribute" >
+| < SORTING: "sorting" >
+| < ASCENDING: "ascending" >
+| < DESCENDING: "descending" >
+| < UCA: "uca" >
+| < RAW: "raw" >
+| < LOWERCASE: "lowercase" >
+| < FUNCTION: "function" >
+| < LOCALE: "locale" >
+| < STRENGTH: "strength" >
+| < PRIMARY: "primary" >
+| < SECONDARY: "secondary" >
+| < TERTIARY: "tertiary" >
+| < QUATERNARY: "quaternary" >
+| < IDENTICAL: "identical" >
+| < STEMMING: "stemming" >
+| < NORMALIZING: "normalizing" >
+| < BOLDING: "bolding" >
+| < BODY: "body" >
+| < HEADER: "header" >
+| < NONE: "none" >
+| < ON: "on" >
+| < OFF: "off" >
+| < TRUE: "true" >
+| < FALSE: "false" >
+| < SYMMETRIC: "symmetric" >
+| < QUERYCOMMAND: "query-command" >
+| < ALIAS: "alias" >
+| < MATCH: "match" >
+| < RANK: "rank" >
+| < LITERAL: "literal" >
+| < EXACT: "exact" >
+| < FILTER: "filter" >
+| < NORMAL: "normal" >
+| < EXACTTERMINATOR: "exact-terminator" >
+| < INDEXINGREWRITE: "indexing-rewrite" >
+| < IGNOREDEFAULTRANKFEATURES: "ignore-default-rank-features" >
+| < ID: "id" >
+| < SOURCE: "source" >
+| < TO: "to" >
+| < DIRECT: "direct" >
+| < ALWAYS: "always" >
+| < ONDEMAND: "on-demand" >
+| < NEVER: "never" >
+| < ENABLEBITVECTORS: "enable-bit-vectors" >
+| < ENABLEONLYBITVECTOR: "enable-only-bit-vector" >
+| < FASTACCESS: "fast-access" >
+| < FASTSEARCH: "fast-search" >
+| < HUGE: "huge" >
+| < PREFETCH: "prefetch" >
+| < NOPREFETCH: "no-prefetch" >
+| < TENSOR_TYPE: "tensor(" (~["(",")"])+ ")" >
+| < TENSOR_VALUE_SL: "value" (" ")* ":" (" ")* ("{"<BRACE_SL_LEVEL_1>) ("\n")? >
+| < TENSOR_VALUE_ML: "value" (<SEARCHLIB_SKIP>)? "{" (["\n"," "])* ("{"<BRACE_ML_LEVEL_1>) (["\n"," "])* "}" ("\n")? >
+| < COMPRESSION: "compression" >
+| < COMPRESSIONLEVEL: "level" >
+| < COMPRESSIONTHRESHOLD: "threshold" >
+| < LZ4: "lz4" >
+| < USEDOCUMENT: "use-document" >
+| < LBRACE: "{" >
+| < RBRACE: "}" >
+| < COLON: ":" >
+| < DOT: "." >
+| < COMMA: "," >
+| < ARRAY: "array" >
+| < WEIGHTEDSET: "weightedset" >
+| < MAP: "map" >
+| < QUESTIONMARK: "?" >
+| < CREATEIFNONEXISTENT: "create-if-nonexistent" >
+| < REMOVEIFZERO: "remove-if-zero" >
+| < MATCHPHASE: "match-phase" >
+| < EVALUATION_POINT: "evaluation-point" >
+| < PRE_POST_FILTER_TIPPING_POINT: "pre-post-filter-tipping-point" >
+| < ORDER: "order" >
+| < MAXFILTERCOVERAGE: "max-filter-coverage" >
+| < MAXHITS: "max-hits" >
+| < FIRSTPHASE: "first-phase" >
+| < SECONDPHASE: "second-phase" >
+| < MACRO: "macro" >
+| < INLINE: "inline" >
+| < ARITY: "arity" >
+| < LOWERBOUND: "lower-bound" >
+| < UPPERBOUND: "upper-bound" >
+| < DENSEPOSTINGLISTTHRESHOLD: "dense-posting-list-threshold" >
+| < SUMMARYFEATURES_SL: "summary-features" (" ")* ":" (~["}","\n"])* ("\n")? >
+| < SUMMARYFEATURES_ML: "summary-features" (<SEARCHLIB_SKIP>)? "{" (~["}"])* "}" >
+| < RANKFEATURES_SL: "rank-features" (" ")* ":" (~["}","\n"])* ("\n")? >
+| < RANKFEATURES_ML: "rank-features" (<SEARCHLIB_SKIP>)? "{" (~["}"])* "}" >
+| < EXPRESSION_SL: "expression" (" ")* ":" (("{"<BRACE_SL_LEVEL_1>)|<BRACE_SL_CONTENT>)* ("\n")? >
+| < EXPRESSION_ML: "expression" (<SEARCHLIB_SKIP>)? "{" (("{"<BRACE_ML_LEVEL_1>)|<BRACE_ML_CONTENT>)* "}" >
+| < #BRACE_SL_LEVEL_1: (("{"<BRACE_SL_LEVEL_2>)|<BRACE_SL_CONTENT>)* "}" >
+| < #BRACE_SL_LEVEL_2: (("{"<BRACE_SL_LEVEL_3>)|<BRACE_SL_CONTENT>)* "}" >
+| < #BRACE_SL_LEVEL_3: <BRACE_SL_CONTENT> "}" >
+| < #BRACE_SL_CONTENT: (~["{","}","\n"])* >
+| < #BRACE_ML_LEVEL_1: (("{"<BRACE_ML_LEVEL_2>)|<BRACE_ML_CONTENT>)* "}" >
+| < #BRACE_ML_LEVEL_2: (("{"<BRACE_ML_LEVEL_3>)|<BRACE_ML_CONTENT>)* "}" >
+| < #BRACE_ML_LEVEL_3: <BRACE_ML_CONTENT> "}" >
+| < #BRACE_ML_CONTENT: (~["{","}"])* >
+| < #SEARCHLIB_SKIP: ([" ","\f","\n","\r","\t"])+ >
+| < RANKPROPERTIES: "rank-properties" >
+| < RERANKCOUNT: "rerank-count" >
+| < NUMTHREADSPERSEARCH: "num-threads-per-search" >
+| < NUMSEARCHPARTITIONS: "num-search-partitions" >
+| < TERMWISELIMIT: "termwise-limit" >
+| < KEEPRANKCOUNT: "keep-rank-count" >
+| < RANKSCOREDROPLIMIT: "rank-score-drop-limit" >
+| < CONSTANTS: "constants" >
+| < FILE: "file" >
+| < IDENTIFIER: ["a"-"z","A"-"Z", "_"] (["a"-"z","A"-"Z","0"-"9","_","-"])* >
+| < QUOTEDSTRING: "\"" ( ~["\""] )* "\"" >
+| < CONTEXT: ["a"-"z","A"-"Z"] (["a"-"z", "A"-"Z", "0"-"9"])* >
+| < DOUBLE: ("-")? (["0"-"9"])+ "." (["0"-"9"])+ >
+| < INTEGER: ("-")? (["0"-"9"])+ >
+| < LONG: ("-")? (["0"-"9"])+"L" >
+| < STRING: (["a"-"z","A"-"Z","_","0"-"9","."])+ >
+| < LESSTHAN: "<" >
+| < GREATERTHAN: ">" >
+| < VARIABLE: "$" <IDENTIFIER> >
+}
+
+// Declare a special skip token for comments.
+SPECIAL_TOKEN :
+{
+ <SINGLE_LINE_COMMENT: "#" (~["\n","\r"])* >
+}
+
+
+// --------------------------------------------------------------------------------
+//
+// Production rules.
+//
+// --------------------------------------------------------------------------------
+
+/**
+ * The rule consumes any search definition and returns the corresponding object. This is the only production that should
+ * ever consume leading newlines.
+ *
+ * @param dir The directory containing the file being parsed.
+ * @return The search definition object.
+ */
+Search search(DocumentTypeManager docMan, String dir) :
+{
+ this.docMan = docMan;
+ Search search;
+}
+{
+ (<NL>)* (search = rootSearch(dir) | search = rootDocument(dir))
+ { return search; }
+}
+
+/**
+ * This rule consumes a proper search block. This and rootDocument() are the only rules that should ever consume
+ * trailing newline tokens.
+ *
+ * @param dir The directory containing the file being parsed.
+ * @return The search definition object.
+ */
+Search rootSearch(String dir) :
+{
+ String name;
+ Search search;
+}
+{
+ ( <SEARCH> name = identifier() { if (!name.matches("[a-zA-Z_][a-zA-Z_:0-9]*")) {
+ deployLogger.log(Level.WARNING, name + " can not be used in YQL+ expressions.");
+ }
+ search = new Search(name, app);
+ rankProfileRegistry.addRankProfile(new DefaultRankProfile(search, rankProfileRegistry));
+ rankProfileRegistry.addRankProfile(new UnrankedRankProfile(search, rankProfileRegistry));}
+ lbrace() (rootSearchItem(search) (<NL>)*)* <RBRACE> (<NL>)* <EOF>)
+ { return search; }
+}
+
+/**
+ * Consumes an element of a search block. This and rootSearch() are the only rules that should ever consume
+ * trailing newline tokens.
+ *
+ * @param search The search object to modify.
+ * @return Null.
+ */
+Object rootSearchItem(Search search) : { }
+{
+ ( document(search)
+ | documentSummary(search)
+ | field(null, search)
+ | index(search, null)
+ | rankProfile(search)
+ | searchStemming(search)
+ | useDocument(search)
+ | structOutside(search)
+ | annotationOutside(search)
+ | fieldSet(search) )
+ { return null; }
+}
+
+/**
+ * Consumes a search definition that contains only documents to be used for inheritance, etc.
+ *
+ * @param dir The directory containing the file being parsed.
+ * @return The search definition object.
+ */
+Search rootDocument(String dir) :
+{
+ Search search = new UnproperSearch();
+}
+{
+ ( (rootDocumentItem(search) (<NL>)*)*<EOF> )
+ { return search; }
+}
+
+/**
+ * Consumes a single item from within a root document node.
+ *
+ * @param search The search object to modify.
+ * @return Null.
+ */
+Object rootDocumentItem(Search search) : { }
+{
+ ( namedDocument(search) )
+ { return null; }
+}
+
+/**
+ * Consumes a use-document statement. This currently does nothing.
+ *
+ * @param search The search object to modify.
+ */
+void useDocument(Search search) : { }
+{
+ <USEDOCUMENT> <COLON> identifier()
+}
+
+/**
+ * Consumes a document element. The name defaults to the search's name, but may be set.
+ *
+ * @param search The search object to add content to.
+ */
+void document(Search search) :
+{
+ String name=search.getName();
+ SDDocumentType document;
+}
+{
+ ( <DOCUMENT> (name = identifier())? (<NL>)* { document = new SDDocumentType(name, search); }
+ [ inheritsDocument(document) (<NL>)* ]
+ <LBRACE> (<NL>)* (documentBody(document, search) (<NL>)*)* <RBRACE> )
+ {
+ search.addDocument(document);
+ }
+}
+
+/**
+ * Consumes a document element, explicitly named
+ *
+ * @param search The search object to add content to.
+ */
+void namedDocument(Search search) :
+{
+ String name;
+ SDDocumentType document;
+}
+{
+ ( <DOCUMENT> name = identifier() (<NL>)* { document = new SDDocumentType(name, search); }
+ [ inheritsDocument(document) (<NL>)* ]
+ <LBRACE> (<NL>)* (documentBody(document, search) (<NL>)*)* <RBRACE> )
+ {
+ search.addDocument(document);
+ }
+}
+
+/**
+ * Consumes a document body block
+ *
+ * @param document The document type to modify.
+ * @param search The search object to add content to.
+ * @return Null.
+ */
+Object documentBody(SDDocumentType document, Search search) :
+{
+}
+{
+ ( annotation(search, document)
+ | compression(document, null)
+ | headercfg(document)
+ | bodycfg(document)
+ | structInside(document, search)
+ | field(document, search) )
+ { return null; }
+}
+
+/**
+ * Consumes a document head block.
+ *
+ * @param document The document type to modify.
+ */
+void headercfg(SDDocumentType document) : { }
+{
+ <HEADER> lbrace() [compression(document, "header") (<NL>)*] <RBRACE>
+}
+
+/**
+ * Consumes a document body block.
+ *
+ * @param document The document type to modify.
+ */
+void bodycfg(SDDocumentType document) : { }
+{
+ <BODY> lbrace() [compression(document, "body") (<NL>)*] <RBRACE>
+}
+
+/**
+ * Consumes a compression block. This can be set in both document header and -body block.
+ *
+ * @param document The document type to modify.
+ * @param name The name of the document block to modify.
+ */
+void compression(SDDocumentType document, String name) :
+{
+ CompressionConfig cfg = new CompressionConfig(CompressionType.LZ4);
+}
+{
+ <COMPRESSION> lbrace() (cfg = compressionItem(cfg) (<NL>)*)* <RBRACE>
+ {
+ if (name == null || name.equals("header")) {
+ document.getDocumentType().getHeaderType().setCompressionConfig(cfg);
+ }
+ if (name == null || name.equals("body")) {
+ document.getDocumentType().getBodyType().setCompressionConfig(cfg);
+ }
+ }
+}
+
+/**
+ * Consumes the body of a compression block.
+ *
+ * @param cfg The compression config to modify.
+ */
+CompressionConfig compressionItem(CompressionConfig cfg) :
+{
+ int val = -1;
+}
+{
+ ( ( <TYPE> <COLON> <LZ4> { cfg = new CompressionConfig(CompressionType.LZ4, cfg.compressionLevel, cfg.threshold); } )
+ | (<COMPRESSIONTHRESHOLD> <COLON> val = integer()) { setCompressionThreshold(cfg, val); }
+ | (<COMPRESSIONLEVEL> <COLON> val = integer()) { setCompressionLevel(cfg, val); }
+ )
+ {
+ return cfg;
+ }
+}
+
+/**
+ * Consumes a document inheritance statement.
+ *
+ * @param document The document type to modify.
+ */
+void inheritsDocument(SDDocumentType document) :
+{
+ String name;
+}
+{
+ <INHERITS> name = identifier() { document.inherit(new DataTypeName(name)); }
+ ( <COMMA> name = identifier() { document.inherit(new DataTypeName(name)); } )*
+}
+
+/**
+ * Consumes a field block from within a document element.
+ *
+ * @param document The document type to modify.
+ * @param search The search object to add content to.
+ */
+void field(SDDocumentType document, Search search) :
+{
+ String name;
+ SDField field;
+ DataType type;
+}
+{
+ <FIELD> name = identifier() <TYPE> type = dataType()
+ {
+ if (name != null && com.yahoo.searchdefinition.Search.isReservedName(name.toLowerCase())) {
+ throw new IllegalArgumentException("Reserved name '" + name + "' can not be used as a field name.");
+ }
+ field = new TemporarySDField(name, type, true, document);
+ }
+ lbrace() (fieldBody(field, search, document) (<NL>)*)* <RBRACE>
+ {
+ if (document != null) {
+ document.addField(field);
+ } else {
+ search.addExtraField(field);
+ }
+ }
+}
+
+void fieldSet(Search search) :
+{
+ String name;
+}
+{
+ <FIELDSET> name = identifier() lbrace()
+ (fieldSetItem(name, search)(<NL>)*)+
+ <RBRACE>
+}
+
+void fieldSetItem(String setName, Search search) :
+{
+ String field;
+ String queryCommand;
+ SDField matchSettings = new SDField(setName, DataType.STRING); // match etc for fieldset represented as SDField or ease of parsing
+}
+{
+ ( <FIELDS><COLON> field=identifier() { search.fieldSets().addUserFieldSetItem(setName, field); }
+ ( <COMMA> field=identifier() { search.fieldSets().addUserFieldSetItem(setName, field); } )* )
+ |
+ ( <QUERYCOMMAND> <COLON> (queryCommand = identifier() | queryCommand = quotedString())) { search.fieldSets().userFieldSets().get(setName).queryCommands().add(queryCommand);}
+ |
+ ( match(matchSettings) ) { matchSettings.applyOperations(); search.fieldSets().userFieldSets().get(setName).setMatching(matchSettings.getMatching());}
+}
+
+/**
+ * This rule consumes a annotation block from within either a document element or a search element.
+ * @param search The search object to add content to.
+ */
+void annotationOutside(Search search) :
+{
+ String name;
+ SDAnnotationType type;
+}
+{
+ <ANNOTATION> name = identifier()
+ {
+ type = new SDAnnotationType(name.trim());
+ }
+ [ inheritsAnnotation(type) (<NL>)* ]
+ lbrace() (type = annotationBody(search, type)) <RBRACE>
+ {
+ if (search.getDocument()==null) throw new IllegalArgumentException("Can't add annotation '"+name+"' to a document type, define a document type first or declare the annotation inside of one.");
+ search.addAnnotation(type);
+ }
+}
+
+/**
+ * This rule consumes a annotation block from within either a document element.
+ * @param document The document object to add content to.
+ */
+void annotation(Search search, SDDocumentType document) :
+{
+ String name;
+ SDAnnotationType type;
+}
+{
+ <ANNOTATION> name = identifier()
+ {
+ type = new SDAnnotationType(name.trim());
+ }
+ [ inheritsAnnotation(type) (<NL>)* ]
+ lbrace() (type = annotationBody(search, type)) <RBRACE>
+ {
+ document.addAnnotation(type);
+ }
+}
+
+
+/**
+ * This rule consumes a single element of an annotation body block.
+ *
+ * @param search The search object to add content to.
+ * @param type The type being built.
+ * @return a modified or new AnnotationType instance
+ */
+SDAnnotationType annotationBody(Search search, SDAnnotationType type) :
+{
+ SDDocumentType struct = new SDDocumentType("annotation." + type.getName(), search);
+}
+{
+ (structFieldDefinition(struct) (<NL>)*)*
+ {
+ if (struct.getFieldCount() > 0) { // Must account for the temporary TemporarySDField.
+ type = new SDAnnotationType(type.getName(), struct, type.getInherits());
+ struct.setStruct(null);
+ }
+ return type;
+ }
+}
+
+void inheritsAnnotation(SDAnnotationType annotation) :
+{
+ String name;
+}
+{
+ <INHERITS> name = identifier() { annotation.inherit(name); }
+}
+
+
+/**
+ * This rule consumes a struct block from within a document element.
+ *
+ * @param search The search object to add content to.
+ */
+void structInside(SDDocumentType document, Search search) :
+{
+ SDDocumentType struct;
+}
+{
+ (
+ struct = structDefinition(search, document)
+ )
+ {
+ document.addType(struct);
+ }
+}
+
+/**
+ * This rule consumes a struct block from within a document element.
+ *
+ * @param search The search object to add content to.
+ */
+void structOutside(Search search) :
+{
+ SDDocumentType struct;
+}
+{
+ (
+ struct = structDefinition(search, search.getDocument())
+ )
+ {
+ search.addType(struct);
+ }
+}
+
+/**
+ * This rule consumes a struct block from within a document element.
+ *
+ * @param search The search object to add content to.
+ */
+SDDocumentType structDefinition(Search search, SDDocumentType repo) :
+{
+ String name;
+ SDDocumentType struct;
+}
+{
+ <STRUCT> name = identifier()
+ {
+ struct = new SDDocumentType(name, search);
+ }
+ lbrace() (structFieldDefinition(struct) (<NL>)*)* <RBRACE>
+ {
+ try {
+ docMan.getDataType(name);
+ throw new ParseException("Reserved name '" + name + "' can not be used to declare a struct.");
+ } catch (IllegalArgumentException e) {
+ // empty
+ }
+ if (repo==null) throw new IllegalArgumentException("Can't add struct '"+name+"' to a document type, define a document type first or declare the struct inside of one.");
+ SDDocumentType sdtype = repo.getOwnedType(struct.getDocumentName());
+ DataType stype = sdtype != null
+ ? sdtype.getStruct()
+ : TemporaryStructuredDataType.create(struct.getName());
+ struct.setStruct(stype);
+ return struct;
+ }
+}
+
+/**
+ * This rule consumes a data type block from within a field element.
+ *
+ * @return The consumed data type.
+ */
+DataType dataType() :
+{
+ String typeName=null;
+ boolean isArrayOldStyle = false;
+ DataType mapType=null;
+ DataType arrayType=null;
+ DataType wsetType=null;
+
+}
+{
+ ( LOOKAHEAD(<ARRAY> <LESSTHAN>) ( <ARRAY> <LESSTHAN> arrayType = dataType() <GREATERTHAN> { return DataType.getArray(arrayType); } )
+ | LOOKAHEAD(<WEIGHTEDSET> <LESSTHAN>) ( <WEIGHTEDSET> <LESSTHAN> wsetType = dataType() <GREATERTHAN> { return DataType.getWeightedSet(wsetType); } )
+ | LOOKAHEAD(<MAP> <LESSTHAN>) ( mapType = mapDataType() { return mapType; } )
+ | LOOKAHEAD(<ANNOTATIONREFERENCE> <LESSTHAN>) ( mapType = annotationRefDataType() { return mapType; } )
+ | ( typeName = identifier() ["[]" { isArrayOldStyle = true; }] )
+ )
+ {
+ DataType type = VespaDocumentType.INSTANCE.getDataType(typeName);
+
+ //is type still null?
+ if (type == null) {
+ //we are basically creating TemporaryStructDataType instances for ANYTHING here!!
+ //we must do this and clean them up later.
+ type = TemporaryStructuredDataType.create(typeName);
+ }
+
+ if (isArrayOldStyle) {
+ deployLogger.log(Level.WARNING, "Data type syntax '" + typeName + "[]' is deprecated, use 'array<" + typeName + ">' instead.");
+ type = DataType.getArray(type);
+ }
+ if ("tag".equalsIgnoreCase(typeName) && type instanceof WeightedSetDataType) ((WeightedSetDataType)type).setTag(true);
+ return type;
+ }
+}
+
+DataType annotationRefDataType() :
+{
+ DataType dataType;
+ String targetName;
+}
+{
+ ( <ANNOTATIONREFERENCE> <LESSTHAN> targetName = identifier() <GREATERTHAN> )
+ {
+ return new TemporaryAnnotationReferenceDataType(targetName);
+ }
+}
+
+DataType mapDataType() :
+{
+ DataType keyType;
+ DataType valType;
+}
+{
+ ( <MAP> <LESSTHAN> keyType = dataType() <COMMA> valType = dataType() <GREATERTHAN> )
+ {
+ return DataType.getMap(keyType, valType);
+ }
+
+}
+
+/* Note: not currently used, remove when decided that map type will not support
+polymorphism */
+DataType wildCardType() :
+{
+}
+{
+(<QUESTIONMARK>) { return DataType.NONE; }
+}
+
+/**
+ * This rule consumes a field block of a struct body.
+ *
+ * @param struct The struct to modify.
+ */
+void structFieldDefinition(SDDocumentType struct) :
+{
+ String name;
+ SDField field;
+ DataType type;
+}
+{
+ <FIELD> name = identifier() <TYPE> type = dataType() {
+ if (name != null && com.yahoo.searchdefinition.Search.isReservedName(name.toLowerCase())) {
+ throw new IllegalArgumentException("Reserved name '" + name + "' can not be used as a field name.");
+ }
+ field = new TemporarySDField(name, type, true, struct);
+ struct.addField(field);
+ }
+ lbrace() (id(field,struct) (<NL>)*)? (match(field) (<NL>)*)* <RBRACE> {
+ }
+}
+
+/**
+ * This rule consumes a struct subfield from a document field body. This is not to be confused with a document
+ * struct's fields, but rather this is a subfield of a document field of type struct.
+ *
+ * @param field The field to modify.
+ * @param search The search object to add content to.
+ * @param document The document type to modify.
+ */
+void structField(FieldOperationContainer field, Search search,SDDocumentType document) :
+{
+ String name;
+ SDField structField;
+}
+{
+ <STRUCTFIELD> name = identifier() {
+ if (name != null && com.yahoo.searchdefinition.Search.isReservedName(name.toLowerCase())) {
+ throw new IllegalArgumentException("Reserved name '" + name + "' can not be used as a field name.");
+ }
+ FieldOperationContainer structFieldOp = new StructFieldOperation(name);
+ field.addOperation((StructFieldOperation) structFieldOp);
+ }
+ lbrace() (structFieldBody(structFieldOp, search, document) (<NL>)*)* <RBRACE>
+}
+
+
+/**
+ * This rule consumes a single element of a field body block.
+ *
+ * @param field The field being built.
+ * @param search The search object to add content to.
+ * @param document The owning document, or null if this is a search field.
+ * @return Null.
+ */
+String fieldBody(SDField field, Search search, SDDocumentType document) : { }
+{
+ ( alias(field) |
+ attribute(field) |
+ body(field) |
+ bolding(field) |
+ fieldStemming(field) |
+ header(field) |
+ id(field, document) |
+ summaryInField(field) |
+ index(search, field) |
+ indexing(field) |
+ indexingRewrite(field) |
+ match(field) |
+ normalizing(field) |
+ queryCommand(field) |
+ rank(field) |
+ rankType(field) |
+ sorting(field, field.getName()) |
+ structField(field, search, document) |
+ summaryTo(field) |
+ weight(field) |
+ weightedset(field) )
+ { return null; }
+}
+
+/**
+ * This rule consumes a single element of a struct subfield body block.
+ * Only elements that make sense for streaming search are allowed,
+ * since only streaming search supports structs for now.
+ *
+ * @param field The field being built.
+ * @param search The search object to add content to.
+ * @param document The owning document, or null if this is a search field.
+ * @return Null.
+ */
+String structFieldBody(FieldOperationContainer field, Search search, SDDocumentType document) : { }
+{
+ ( summaryInField(field) |
+ indexing(field) |
+ match(field) |
+ queryCommand(field) |
+ structField(field, search, document) |
+ summaryTo(field) )
+ { return null; }
+}
+
+/**
+ * This rule consumes an indexing block of a field element.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object indexing(FieldOperationContainer field) : { }
+{
+ ( <INDEXING> ( (<COLON> indexingOperation(field, false)) | indexingOperation(field, true) ) )
+ { return null; }
+}
+
+/**
+ * This rule consumes an IL script block. This is expected to consume trailing newlines.
+ *
+ * @param field The field to modify.
+ */
+void indexingOperation(FieldOperationContainer field, boolean multiLine) : { }
+{
+ { field.addOperation(newIndexingOperation(multiLine)); }
+}
+
+/**
+ * This rule consumes a summary-to statement of a field element.
+ *
+ * @param field The field to modify.
+ */
+void summaryTo(FieldOperationContainer field) :
+{
+ SummaryToOperation op = new SummaryToOperation();
+ String destination;
+ String name = field.getName();
+}
+{
+ <SUMMARYTO> [name = identifier()] <COLON> destination = identifier()
+ {
+ op.setName(name);
+ op.addDestination(destination);
+ }
+ ( <COMMA> destination = identifier() {op.addDestination(destination); } )*
+ {
+ field.addOperation(op);
+ }
+}
+
+
+/**
+ * This rule consumes a weight statement of a field element.
+ *
+ * @param field The field to modify.
+ */
+void weight(FieldOperationContainer field) :
+{
+ int num;
+}
+{
+ <WEIGHT> <COLON> num = integer()
+ {
+ WeightOperation op = new WeightOperation();
+ op.setWeight(num);
+ field.addOperation(op);
+ }
+}
+
+/**
+ * This rule consumes a weighted set statement of a field element.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object weightedset(FieldOperationContainer field) :
+{
+ WeightedSetOperation op = new WeightedSetOperation();
+}
+{
+ <WEIGHTEDSET> ( (<COLON> weightedsetBody(op))
+ | (lbrace() (weightedsetBody(op) (<NL>)*)* <RBRACE>) )
+ {
+ field.addOperation(op);
+ return null;
+ }
+}
+
+/**
+ * This rule consumes one body item of a weighted set block.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object weightedsetBody(WeightedSetOperation field) : { }
+{
+ ( <CREATEIFNONEXISTENT> { field.setCreateIfNonExistent(true); }
+ | <REMOVEIFZERO> { field.setRemoveIfZero(true); } )
+ {
+ return null;
+ }
+}
+
+/**
+ * This rule consumes a rank-type statement of a field element.
+ *
+ * @param field The field to modify.
+ */
+void rankType(FieldOperationContainer field) :
+{
+ String typeName;
+ String indexName = null;
+}
+{
+ <RANKTYPE> [indexName = identifier()] <COLON> typeName = identifier()
+ {
+ RankTypeOperation op = new RankTypeOperation();
+ op.setType(RankType.fromString(typeName));
+ op.setIndexName(indexName);
+ field.addOperation(op);
+ }
+}
+
+/**
+ * This rule consumes an attribute statement of a field element.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object attribute(FieldOperationContainer field) :
+{
+ String name = field.getName();
+}
+{
+ <ATTRIBUTE> [name = identifier()]
+ {
+ AttributeOperation op = new AttributeOperation(name);
+ }
+ ( (<COLON> attributeSetting(field, op, name))
+ | (lbrace() (attributeSetting(field, op, name) (<NL>)*)* <RBRACE>) )
+ {
+ field.addOperation(op);
+ return null;
+ }
+}
+
+Object sorting(FieldOperationContainer field, String name) :
+{
+ SortingOperation op = new SortingOperation(name);
+}
+{
+ <SORTING>
+ ( (<COLON> sortingSetting(op, name))
+ | (lbrace() (sortingSetting(op, name) (<NL>)*)* <RBRACE>) )
+ {
+ field.addOperation(op);
+ return null;
+ }
+}
+
+Object sortingSetting(SortingOperation sorting, String attributeName) :
+{
+ String locale;
+}
+{
+ (
+ <ASCENDING> { sorting.setAscending(); }
+ | <DESCENDING> { sorting.setDescending(); }
+ | <FUNCTION> <COLON> (
+ <UCA> { sorting.setFunction(Sorting.Function.UCA); }
+ | <RAW> { sorting.setFunction(Sorting.Function.RAW); }
+ | <LOWERCASE> { sorting.setFunction(Sorting.Function.LOWERCASE); }
+ )
+ | <STRENGTH> <COLON> (
+ <PRIMARY> { sorting.setStrength(Sorting.Strength.PRIMARY); }
+ | <SECONDARY> { sorting.setStrength(Sorting.Strength.SECONDARY); }
+ | <TERTIARY> { sorting.setStrength(Sorting.Strength.TERTIARY); }
+ | <QUATERNARY> { sorting.setStrength(Sorting.Strength.QUATERNARY); }
+ | <IDENTICAL> { sorting.setStrength(Sorting.Strength.IDENTICAL); }
+ )
+ | <LOCALE> <COLON> locale = identifier() { sorting.setLocale(locale); }
+ )
+ { return null; }
+}
+
+/**
+ * This rule consumes a single attribute setting statement of an attribute element.
+ *
+ * @param field The field to modify.
+ * @param attributeName The name of the attribute to change.
+ * @return Null.
+ */
+Object attributeSetting(FieldOperationContainer field, AttributeOperation attribute, String attributeName) :
+{
+}
+{
+ (
+ <HUGE> { attribute.setHuge(true); }
+ | <FASTSEARCH> { attribute.setFastSearch(true); }
+ | <FASTACCESS> { attribute.setFastAccess(true); }
+ | <ENABLEBITVECTORS> { attribute.setEnableBitVectors(true); }
+ | <ENABLEONLYBITVECTOR> { attribute.setEnableOnlyBitVector(true); }
+ | <NOPREFETCH> {
+ deployLogger.log(Level.WARNING, field + ": 'attribute : no-prefetch' is deprecated and has no effect.");
+ attribute.setPrefetch(false);
+ }
+ | <PREFETCH> {
+ deployLogger.log(Level.WARNING, field + ": 'attribute : prefetch' is deprecated and has no effect.");
+ attribute.setPrefetch(true);
+ }
+ | sorting(field, attributeName)
+ | <ALIAS> { String alias; String aliasedName=attributeName; } [aliasedName = identifier()] <COLON> alias = identifier() {
+ attribute.setDoAlias(true);
+ attribute.setAlias(alias);
+ attribute.setAliasedName(aliasedName);
+ }
+ | attributeTensorType(attribute)
+ )
+ { return null; }
+}
+
+/**
+ * This rule consumes a tensor type statement for an attribute element.
+ *
+ * @param attribute The attribute to modify.
+ * @return Null.
+ */
+Object attributeTensorType(AttributeOperation attribute) :
+{
+ String tensorTypeString;
+ TensorType tensorType;
+}
+{
+ <TENSOR_TYPE>
+ {
+ tensorTypeString = token.image;
+ try {
+ tensorType = TensorType.fromSpec(tensorTypeString);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("For attribute field '" + attribute.getName() +
+ "': Illegal tensor type spec: " + e.getMessage());
+ }
+ attribute.setTensorType(tensorType);
+ }
+ { return null; }
+}
+
+/**
+ * This rule consumes a summary statement defined inside a document-summary block.
+ *
+ * @param document The document summary to modify.
+ * @return Null.
+ */
+Object summaryInDocument(DocumentSummary document) :
+{
+ String name;
+ DataType type;
+ SummaryField summary;
+
+}
+{
+ <SUMMARY> name = identifier() { }
+ <TYPE> type = dataType() {
+ summary = new SummaryField(name, type);
+ summary.setVsmCommand(SummaryField.VsmCommand.FLATTENSPACE);
+
+ SummaryInFieldLongOperation op = new SummaryInFieldLongOperation();
+
+ }
+ lbrace() (summaryItem(op) (<NL>)*)* <RBRACE>
+ {
+ if (op.destinationIterator().hasNext()) {
+ throw new ParseException("Summaries defined in a document-summary section " +
+ "can not have a 'to' line.");
+ }
+
+ op.applyToSummary(summary);
+
+ document.add(summary);
+ return null;
+ }
+}
+
+/**
+ * The rule consumes a summary statement defined inside a field.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object summaryInField(FieldOperationContainer field) :
+{
+ SummaryInFieldOperation summary;
+}
+{
+ ( <SUMMARY> ( LOOKAHEAD(2) summary = summaryInFieldShort(field)
+ | summary = summaryInFieldLong(field)) )
+ {
+ field.addOperation(summary);
+ return null;
+ }
+}
+
+/**
+ * This rule consumes a single-line summary field.
+ *
+ * @param field The field to modify.
+ * @return The consumed summary field.
+ */
+SummaryInFieldOperation summaryInFieldShort(FieldOperationContainer field) :
+{
+ String name = field.getName();
+ SummaryField ret;
+}
+{
+ [ name = identifier() ]
+ {
+ SummaryInFieldShortOperation op = new SummaryInFieldShortOperation(name);
+ }
+ <COLON> ( <DYNAMIC> { op.setTransform(SummaryTransform.DYNAMICTEASER);
+ op.addSource(name);
+ }
+ | (<FULL> | <STATIC>) { op.setTransform(SummaryTransform.NONE); } )
+ { return op; }
+}
+
+/**
+ * This rule consumes a multi-line summary field.
+ *
+ * @return The consumed summary field.
+ */
+SummaryInFieldOperation summaryInFieldLong(FieldOperationContainer field) :
+{
+ String name = field.getName();
+ DataType type = null;
+}
+{
+ ( [ name = identifier() [ <TYPE> type = dataType() ] ]
+ lbrace()
+ {
+ SummaryInFieldLongOperation op = new SummaryInFieldLongOperation(name);
+ op.setType(type);
+ }
+ (summaryItem(op) (<NL>)*)* <RBRACE> )
+ { return op; }
+}
+
+/**
+ * This rule consumes an item of a summary field block.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object summaryItem(SummaryInFieldLongOperation field) : { }
+{
+ ( summaryTransform(field)
+ | summaryBolding(field)
+ | summarySourceList(field)
+ | summaryDestinationList(field)
+ | summaryProperties(field) )
+ { return null; }
+}
+
+/**
+ * This rule consumes a transform statement for a summary field element.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object summaryTransform(SummaryInFieldOperation field) : { }
+{
+ ( <DYNAMIC> { field.setTransform(SummaryTransform.DYNAMICTEASER); }
+ | (<FULL> | <STATIC>) { field.setTransform(SummaryTransform.NONE); } )
+ { return null; }
+}
+
+/**
+ * This rule consumes a bolding statement for a summary field element.
+ *
+ * @param field The summary field to modify.
+ */
+void summaryBolding(SummaryInFieldLongOperation field) :
+{
+ boolean bold;
+}
+{
+ <BOLDING> <COLON> bold = bool()
+ { field.setBold(bold); }
+}
+
+/**
+ * This rule consumes a source-list statement for a summary field element.
+ *
+ * @param field The summary field to modify.
+ */
+void summarySourceList(SummaryInFieldOperation field) :
+{
+ String str;
+}
+{
+ ( <SOURCE> <COLON> str = identifier() { field.addSource(str); }
+ ( <COMMA> str = identifier() { field.addSource(str); } )* ) +
+}
+
+/**
+ * This rule consumes a destination-list statement for a summary field element.
+ *
+ * @param field The summary field to modify.
+ */
+void summaryDestinationList(SummaryInFieldLongOperation field) :
+{
+ String str;
+}
+{
+ <TO> <COLON> str = identifier() { field.addDestination(str); }
+ ( <COMMA> str = identifier() { field.addDestination(str); } )*
+}
+
+/**
+ * This rule consumes properties for a summary field element.
+ *
+ * @param field The summary field to modify.
+ */
+void summaryProperties(SummaryInFieldLongOperation field) : { }
+{
+ <PROPERTIES> lbrace() (summaryProperty(field) <NL>)+ <RBRACE>
+}
+
+/**
+ * This rule consumes a single summary property pair for a summary field element.
+ *
+ * @param field The summary field to modify.
+ */
+void summaryProperty(SummaryInFieldLongOperation field) :
+{
+ String name, value;
+}
+{
+ name = identifier() <COLON> (value = identifier() | value = quotedString())
+ { field.addProperty(new SummaryField.Property(name, value)); }
+}
+
+/**
+ * This rule consumes a stemming block of a field element.
+ *
+ * @param field The field to modify.
+ */
+void fieldStemming(FieldOperationContainer field) :
+{
+ String setting;
+ StemmingOperation op = new StemmingOperation();
+}
+{
+ <STEMMING> <COLON> setting = identifier()
+ {
+ op.setSetting(setting);
+ field.addOperation(op);
+ }
+}
+
+/**
+ * This rule consumes a stemming statement for a search element.
+ *
+ * @param search The search to modify.
+ */
+void searchStemming(Search search) :
+{
+ String setting;
+}
+{
+ <STEMMING> <COLON> setting = identifier()
+ { search.setStemming(Stemming.get(setting)); }
+}
+
+/**
+ * This rule consumes a normalizing statement of a field element. At the moment, this can only be used to turn off
+ * normalizing.
+ *
+ * @param field The field to modify.
+ */
+void normalizing(FieldOperationContainer field) :
+{
+ String setting;
+}
+{
+ <NORMALIZING> <COLON> setting = identifier()
+ {
+ field.addOperation(new NormalizingOperation(setting));
+ }
+}
+
+/**
+ * This rule consumes a bolding statement of a field element.
+ *
+ * @param field The field to modify.
+ */
+void bolding(FieldOperationContainer field) :
+{
+ boolean bold;
+}
+{
+ <BOLDING> <COLON> bold = bool()
+ {
+ field.addOperation(new BoldingOperation(bold));
+ }
+}
+
+/**
+ * This rule consumes a body statement of a field element.
+ *
+ * @param field The field to modify.
+ */
+void body(SDField field) : { }
+{
+ <BODY>
+ {
+ field.setHeader(false);
+ field.setHeaderOrBodyDefined(true);
+ }
+}
+
+/**
+ * This rule consumes a header statement of a field element.
+ *
+ * @param field The field to modify.
+ */
+void header(SDField field) : { }
+{
+ <HEADER>
+ {
+ field.setHeader(true);
+ field.setHeaderOrBodyDefined(true);
+ }
+}
+
+void queryCommand(FieldOperationContainer container) :
+{
+ String command;
+ QueryCommandOperation field = new QueryCommandOperation();
+}
+{
+ <QUERYCOMMAND> <COLON> command = identifier()
+ {
+ field.addQueryCommand(command);
+ container.addOperation(field);
+ }
+}
+
+void alias(FieldOperationContainer container) :
+{
+ String aliasedName = null;
+ String alias;
+}
+{
+ <ALIAS> [aliasedName = identifier()] <COLON> alias = identifier()
+ {
+ AliasOperation op = new AliasOperation(aliasedName, alias);
+ container.addOperation(op);
+ }
+}
+
+Object match(FieldOperationContainer field) : { }
+{
+ <MATCH> ( (<COLON> matchType(field))
+ | (lbrace() (matchItem(field) (<NL>)*)* <RBRACE>) )
+ { return null; }
+}
+
+/**
+ * This rule consumes a single match item for a match block.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object matchItem(FieldOperationContainer field) : { }
+{
+ ( matchType(field) | exactTerminator(field) | gramSize(field) | matchSize(field) )
+ { return null; }
+}
+
+Object matchType(FieldOperationContainer container) :
+{
+ MatchOperation field = new MatchOperation();
+}
+{
+ ( <MTOKEN> { field.setMatchingType(Matching.Type.TEXT); } // Deprecated synonym to TEXT
+ | <TEXT> { field.setMatchingType(Matching.Type.TEXT); }
+ | <WORD> { field.setMatchingType(Matching.Type.WORD); }
+ | <EXACT> { field.setMatchingType(Matching.Type.EXACT); }
+ | <GRAM> { field.setMatchingType(Matching.Type.GRAM); }
+ | <PREFIX> { field.setMatchingAlgorithm(Matching.Algorithm.PREFIX); }
+ | <SUBSTRING> { field.setMatchingAlgorithm(Matching.Algorithm.SUBSTRING); }
+ | <SUFFIX> { field.setMatchingAlgorithm(Matching.Algorithm.SUFFIX); } )
+ {
+ container.addOperation(field);
+ return null;
+ }
+}
+
+void exactTerminator(FieldOperationContainer container) :
+{
+ String terminator;
+ MatchOperation field = new MatchOperation();
+}
+{
+ <EXACTTERMINATOR> <COLON> terminator = quotedString()
+ {
+ field.setExactMatchTerminator(terminator);
+ container.addOperation(field);
+ }
+}
+
+void gramSize(FieldOperationContainer container) :
+{
+ int gramSize;
+ MatchOperation field = new MatchOperation();
+}
+{
+ <GRAMSIZE> <COLON> gramSize = integer()
+ {
+ field.setGramSize(gramSize);
+ container.addOperation(field);
+ }
+}
+
+void matchSize(FieldOperationContainer container) :
+{
+ int matchSize;
+ MatchOperation field = new MatchOperation();
+}
+{
+ <MAXLENGTH> <COLON> matchSize = integer()
+ {
+ field.setMaxLength(matchSize);
+ container.addOperation(field);
+ }
+}
+/**
+ * Consumes a rank statement of a field element.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object rank(FieldOperationContainer field) :
+{
+ RankOperation op = new RankOperation();
+}
+{
+ <RANK> ( (<COLON> rankSetting(op))
+ | (lbrace() (rankSetting(op) (<NL>)*)* <RBRACE>) )
+ {
+ field.addOperation(op);
+ return null;
+ }
+}
+
+/**
+ * Consumes a single rank setting of a rank statement.
+ *
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object rankSetting(RankOperation field) : { }
+{
+ ( <LITERAL> { field.setLiteral(true); }
+ | <NORMAL> { field.setNormal(true); }
+ | <FILTER> { field.setFilter(true); } )
+ { return null; }
+}
+
+/**
+ * Consumes an id statement of a field body block.
+ *
+ * @param field The field to modify.
+ * @param document The document type to modify.
+ */
+void id(FieldOperationContainer field, SDDocumentType document) :
+{
+ int fieldId;
+ IdOperation op = new IdOperation();
+}
+{
+ <ID> <COLON> fieldId = integer()
+ {
+ op.setDocument(document);
+ op.setFieldId(fieldId);
+ field.addOperation(op);
+ }
+}
+
+/**
+ * Consumes an indexing-rewrite statement of a field body block.
+ *
+ * @param field The field to modify.
+ */
+void indexingRewrite(FieldOperationContainer field) : { }
+{
+ <INDEXINGREWRITE> <COLON> <NONE>
+ { field.addOperation(new IndexingRewriteOperation()); }
+}
+
+/**
+ * Consumes a document-summary block from within a search block.
+ *
+ * @param search The search object to add content to.
+ * @return Null.
+ */
+Object documentSummary(Search search) :
+{
+ String name;
+ DocumentSummary summary;
+}
+{
+ ( ( <DOCUMENTSUMMARY> |
+ <SUMMARY> { deployLogger.log(Level.WARNING, "Directive 'summary' is deprecated, use 'document-summary' instead."); } )
+ name = identifier() { search.addSummary(summary = new DocumentSummary(name)); }
+ lbrace() (documentSummaryItem(summary) (<NL>)*)* <RBRACE> )
+ { return null; }
+}
+
+/**
+ * Consumes a single document-summary item.
+ *
+ * @param summary The document summary to modify.
+ * @return Null.
+ */
+Object documentSummaryItem(DocumentSummary summary) : { }
+{
+ summaryInDocument(summary)
+ { return null; }
+}
+
+/**
+ * Consumes an index block for a field element.
+ *
+ * @param search The search object to add content to.
+ * @param field The field to modify.
+ * @return Null.
+ */
+Object index(Search search, FieldOperationContainer field) :
+{
+ IndexOperation op = new IndexOperation();
+ String indexName = (field != null) ? field.getName() : null;
+}
+{
+ <INDEX> [indexName = identifier()]
+ {
+ if (indexName == null) {
+ throw new ParseException("Index statements outside fields must have an explicit name.");
+ }
+ op.setIndexName(indexName);
+ }
+ ( (<COLON> indexBody(op) (<COMMA> indexBody(op))*) |
+ (lbrace() (indexBody(op) (<NL>)*)* <RBRACE>) )
+ {
+ if (field == null) {
+
+ Index index = new Index(indexName);
+ op.applyToIndex(index);
+ search.addIndex(index);
+ } else {
+ field.addOperation(op);
+ }
+ return null;
+ }
+}
+
+/**
+ * Consumes a single index statement for an index block.
+ *
+ * @param index The index to modify.
+ * @return Null.
+ */
+Object indexBody(IndexOperation index) :
+{
+ String str;
+ int arity;
+ long num;
+ double threshold;
+}
+{
+ ( <PREFIX> { index.setPrefix(true); }
+ | <ALIAS> <COLON> str = identifier() { index.addAlias(str); }
+ | <STEMMING> <COLON> str = identifier() { index.setStemming(str); }
+ | <RISE> {
+ throw new ParseException("'index:rise' is no longer an option. Use 'indexing:attribute' instead. " +
+ "If it is a weighted set field you should also add 'attribute:fast-search'." +
+ "This change will require refeeding.");
+ }
+ | <ARITY> <COLON> arity = integer() { index.setArity(arity); }
+ | <LOWERBOUND> <COLON> num = consumeLong() { index.setLowerBound(num); }
+ | <UPPERBOUND> <COLON> num = consumeLong() { index.setUpperBound(num); }
+ | <DENSEPOSTINGLISTTHRESHOLD> <COLON> threshold = consumeFloat() { index.setDensePostingListThreshold(threshold); }
+ )
+ { return null; }
+}
+
+/**
+ * Consumes a rank-profile block of a search element.
+ *
+ * @param search The search object to add content to.
+ */
+void rankProfile(Search search) :
+{
+ String name;
+ RankProfile profile;
+}
+{
+ ( <RANKPROFILE> name = identifier()
+ {
+ if ("default".equals(name)) {
+ profile = rankProfileRegistry.getRankProfile(search, "default");
+ } else {
+ profile = new RankProfile(name, search, rankProfileRegistry);
+ }
+ }
+ [inheritsRankProfile(profile)]
+ lbrace() (rankProfileItem(profile) (<NL>)*)* <RBRACE> )
+ {
+ rankProfileRegistry.addRankProfile(profile);
+ }
+}
+
+/**
+ * This rule consumes a single statement for a rank-profile block.
+ *
+ * @param profile The rank profile to modify.
+ * @return Null.
+ */
+Object rankProfileItem(RankProfile profile) : { }
+{
+ ( fieldRankType(profile)
+ | fieldWeight(profile)
+ | fieldRankFilter(profile)
+ | firstPhase(profile)
+ | matchPhase(profile)
+ | macro(profile)
+ | ignoreRankFeatures(profile)
+ | numThreadsPerSearch(profile)
+ | numSearchPartitions(profile)
+ | termwiseLimit(profile)
+ | rankFeatures(profile)
+ | rankProperties(profile)
+ | secondPhase(profile)
+ | rankDegradation(profile)
+ | constants(profile)
+ | summaryFeatures(profile) )
+ { return null; }
+}
+
+/**
+ * This rule consumes an inherits statement of a rank-profile.
+ *
+ * @param profile The profile to modify.
+ */
+void inheritsRankProfile(RankProfile profile) :
+{
+ String str;
+}
+{
+ <INHERITS> str = identifier() { profile.setInherited(str); }
+}
+
+/**
+ * This rule consumes a function statement of a rank-profile.
+ *
+ * @param profile The profile to modify.
+ */
+void macro(RankProfile profile) :
+{
+ String macro, param, expr;
+ boolean inline = false;
+}
+{
+ ( <MACRO> inline = inline() macro = identifier() [ "$" { macro = macro + token.image; } ]
+ "(" { profile.addMacro(macro, inline); }
+ [ param = identifier() { profile.getMacros().get(macro).addParam(param); }
+ ( <COMMA> param = identifier() { profile.getMacros().get(macro).addParam(param); } )* ]
+ ")"
+ lbrace() expr = expression() (<NL>)* <RBRACE> )
+ { profile.getMacros().get(macro).setTextualExpression(expr); }
+}
+
+boolean inline() :
+{
+}
+{
+ ( <INLINE> { return true; } ) ?
+ { return false; }
+}
+
+/**
+ * This rule consumes a match-phase block of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void matchPhase(RankProfile profile) :
+{
+ MatchPhaseSettings settings = new MatchPhaseSettings();
+}
+{
+ <MATCHPHASE> lbrace() (matchPhaseItem(settings) (<NL>)*)* <RBRACE>
+ {
+ settings.checkValid();
+ profile.setMatchPhaseSettings(settings);
+ }
+}
+
+void matchPhaseItem(MatchPhaseSettings settings) :
+{
+ String str;
+ int num;
+ double multiplier;
+ double coverage;
+}
+{
+ ( <ATTRIBUTE> <COLON> str = identifier() { settings.setAttribute(str); }
+ | diversity(settings)
+ | <ORDER> <COLON> ( <ASCENDING> { settings.setAscending(true); }
+ | <DESCENDING> { settings.setAscending(false); } )
+ | <MAXHITS> <COLON> num = integer() { settings.setMaxHits(num); }
+ | <MAXFILTERCOVERAGE> <COLON> coverage = consumeFloat() { settings.setMaxFilterCoverage(coverage); }
+ | <EVALUATION_POINT> <COLON> multiplier = consumeFloat() { settings.setEvaluationPoint(multiplier); }
+ | <PRE_POST_FILTER_TIPPING_POINT> <COLON> multiplier = consumeFloat() { settings.setPrePostFilterTippingPoint(multiplier); }
+ )
+ { return; }
+}
+
+/**
+ * This rule consumes a diversity block of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void diversity(MatchPhaseSettings profile) :
+{
+ DiversitySettings settings = new DiversitySettings();
+}
+{
+ <DIVERSITY> lbrace() (diversityItem(settings) (<NL>)*)* <RBRACE>
+ {
+ profile.setDiversity(settings);
+ }
+}
+
+void diversityItem(DiversitySettings settings) :
+{
+ String str;
+ int num;
+ double multiplier;
+}
+{
+ ( <ATTRIBUTE> <COLON> str = identifier() { settings.setAttribute(str); }
+ | <MIN_GROUPS> <COLON> num = integer() { settings.setMinGroups(num); }
+ | <CUTOFF_FACTOR> <COLON> multiplier = consumeFloat() { settings.setCutoffFactor(multiplier); }
+ | <CUTOFF_STRATEGY> <COLON>
+ ( <STRICT> { settings.setCutoffStrategy(Diversity.CutoffStrategy.strict); }
+ | <LOOSE> { settings.setCutoffStrategy(Diversity.CutoffStrategy.loose); }
+ )
+ )
+ { return; }
+}
+
+
+
+/**
+ * Consumes the first-phase block of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void firstPhase(RankProfile profile) :
+{
+ String exp;
+}
+{
+ <FIRSTPHASE> lbrace() (firstPhaseItem(profile) (<NL>)*)* <RBRACE>
+}
+
+Object firstPhaseItem(RankProfile profile) :
+{
+ String expression;
+ int rerankCount;
+ double dropLimit;
+}
+{
+ ( expression = expression() { profile.setFirstPhaseRankingString(expression); }
+ | (<KEEPRANKCOUNT> <COLON> rerankCount = integer()) { profile.setKeepRankCount(rerankCount); }
+ | (<RANKSCOREDROPLIMIT> <COLON> dropLimit = consumeFloat()) { profile.setRankScoreDropLimit(dropLimit); }
+ )
+ { return null; }
+}
+
+/**
+ * Consumes the second-phase block of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void secondPhase(RankProfile profile) : { }
+{
+ <SECONDPHASE> lbrace() (secondPhaseItem(profile) (<NL>)*)* <RBRACE>
+}
+
+/**
+ * Consumes a statement for a second-phase block.
+ *
+ * @param profile The rank profile to modify.
+ * @return Null.
+ */
+Object secondPhaseItem(RankProfile profile) :
+{
+ String expression;
+ int rerankCount;
+}
+{
+ ( expression = expression() { profile.setSecondPhaseRankingString(expression); }
+ | (<RERANKCOUNT> <COLON> rerankCount = integer()) { profile.setRerankCount(rerankCount); }
+ )
+ { return null; }
+}
+
+/**
+ * This rule consumes a summary-features block of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ * @return Null.
+ */
+Object summaryFeatures(RankProfile profile) :
+{
+ String features;
+}
+{
+ ( <SUMMARYFEATURES_SL> { features = token.image.substring(token.image.indexOf(":") + 1).trim(); } |
+ <SUMMARYFEATURES_ML> { features = token.image.substring(token.image.indexOf("{") + 1,
+ token.image.lastIndexOf("}")).trim(); } )
+ {
+ profile.addSummaryFeatures(getFeatureList(features));
+ return null;
+ }
+}
+
+/**
+ * This rule consumes a rank-features block of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ * @return Null.
+ */
+Object rankFeatures(RankProfile profile) :
+{
+ String features;
+}
+{
+ ( <RANKFEATURES_SL> { features = token.image.substring(token.image.indexOf(":") + 1).trim(); } |
+ <RANKFEATURES_ML> { features = token.image.substring(token.image.indexOf("{") + 1,
+ token.image.lastIndexOf("}")).trim(); } )
+ {
+ profile.addRankFeatures(getFeatureList(features));
+ return null;
+ }
+}
+
+/**
+ * This rule consumes a ignore-default-rank-features statement for a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void ignoreRankFeatures(RankProfile profile) : { }
+{
+ <IGNOREDEFAULTRANKFEATURES> { profile.setIgnoreDefaultRankFeatures(true); }
+}
+
+/**
+ * This rule consumes a num-threads-per-search statement for a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void numThreadsPerSearch(RankProfile profile) :
+{
+ int num;
+}
+{
+ (<NUMTHREADSPERSEARCH> <COLON> num = integer()) { profile.setNumThreadsPerSearch(num); }
+}
+
+/**
+ * This rule consumes a num-search-partitions statement for a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void numSearchPartitions(RankProfile profile) :
+{
+ int num;
+}
+{
+ (<NUMSEARCHPARTITIONS> <COLON> num = integer()) { profile.setNumSearchPartitions(num); }
+}
+
+/**
+ * This rule consumes a num-threads-per-search statement for a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void termwiseLimit(RankProfile profile) :
+{
+ double num;
+}
+{
+ (<TERMWISELIMIT> <COLON> num = consumeFloat()) { profile.setTermwiseLimit(num); }
+}
+/**
+ * This rule consumes a rank-properties block of a rank profile. There is a little trick within this rule to allow the
+ * final rank property to skip the terminating newline token.
+ *
+ * @param profile The rank profile to modify.
+ */
+void rankProperties(RankProfile profile) : { }
+{
+ <RANKPROPERTIES> lbrace() (LOOKAHEAD(rankPropertyItem() <COLON> rankPropertyItem() <NL>)
+ rankProperty(profile) (<NL>)+)* [rankProperty(profile)] <RBRACE>
+}
+
+/**
+ * This rule consumes a single rank property pair for a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void rankProperty(RankProfile profile) :
+{
+ String key, val;
+}
+{
+ key = rankPropertyItem() <COLON> val = rankPropertyItem()
+ { profile.addRankProperty(key, val); }
+}
+
+
+/**
+ * This rule consumes a single rank property for a rank-properties block.
+ *
+ * @return The token image of the consumed item.
+ */
+String rankPropertyItem() :
+{
+ String image, ret = "";
+}
+{
+ ( ( image = identifier() { ret += image; }
+ | image = quotedString() { ret += image; }
+ | ( "(" | ")" | <DOT> | <COMMA> ) { ret += token.image; } )+ )
+ { return ret; }
+}
+
+/**
+ * This rule consumes a field-weight statement of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void fieldWeight(RankProfile profile) :
+{
+ Integer num;
+ String name;
+}
+{
+ <WEIGHT> name = identifier() <COLON> num = integer()
+ { profile.addRankSetting(name, RankProfile.RankSetting.Type.WEIGHT, num); }
+}
+
+/**
+ * This rule consumes a rank-type statement of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void fieldRankType(RankProfile profile) :
+{
+ String name;
+ String type;
+}
+{
+ <RANKTYPE> name = identifier() <COLON> type = identifier()
+ { profile.addRankSetting(name, RankProfile.RankSetting.Type.RANKTYPE, RankType.fromString(type)); }
+}
+
+/**
+ * This rule consumes a rank filter statement of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+void fieldRankFilter(RankProfile profile) :
+{
+ String name;
+}
+{
+ <RANK> name = identifier() <COLON> <FILTER>
+ { profile.addRankSetting(name, RankProfile.RankSetting.Type.PREFERBITVECTOR, new Boolean(true)); }
+}
+
+/**
+ * This rule consumes part of a rank-degradation statement of a rank profile.
+ */
+void rankDegradationBinSize() :
+{
+ double freq;
+}
+{
+ <RPBINSIZE> <COLON> freq = consumeFloat()
+ { deployLogger.log(Level.WARNING, "Specifying 'doc-frequency' in 'rank-degradation' is deprecated and has no effect."); }
+}
+
+
+/**
+ * This rule consumes part of a rank-degradation statement of a rank profile.
+ */
+void rankDegradationBinLow() :
+{
+ int n;
+}
+{
+ <RPBINLOW> <COLON> n = integer()
+ { deployLogger.log(Level.WARNING, "Specifying 'min-fullrank-docs' in 'rank-degradation' is deprecated and has no effect."); }
+}
+
+
+/**
+ * This rule consumes part of a rank-degradation statement of a rank profile.
+ */
+void rankDegradationPosbinSize() :
+{
+ double avgOcc;
+}
+{
+ <RPPOSBINSIZE> <COLON> avgOcc = consumeFloat()
+ { deployLogger.log(Level.WARNING, "Specifying 'occurrences-per-doc' in 'rank-degradation' is deprecated and has no effect."); }
+}
+
+
+/**
+ * This rule consumes part of a rank-degradation statement of a rank profile.
+ */
+Object rankDegradationItem() :
+{
+}
+{
+ ( rankDegradationBinSize()
+ | rankDegradationBinLow()
+ | rankDegradationPosbinSize() )
+ { return null; }
+}
+
+/**
+ * This rule consumes a rank-degradation statement of a rank profile.
+ *
+ * @param profile The rank profile to modify.
+ */
+Object rankDegradation(RankProfile profile) :
+{
+ double freq;
+}
+{
+ ( <RANKDEGRADATIONFREQ> <COLON> freq = consumeFloat()
+ { deployLogger.log(Level.WARNING, "Specifying 'rank-degradation-frequency' in 'rank-profile' is deprecated and has no effect."); }
+ | <RANKDEGRADATION> lbrace() ( rankDegradationItem() (<NL>)*)+ <RBRACE>
+ )
+ {
+ return null;
+ }
+}
+
+/**
+ * Consumes a set of constants available in ranking expressions in the enclosing profile.
+ */
+void constants(RankProfile profile) :
+{
+ String name;
+}
+{
+ <CONSTANTS> <LBRACE> (<NL>)*
+ ( name = identifier() ( constantValue(profile, name) |
+ constantTensor(profile, name) ) (<NL>)* )*
+ <RBRACE>
+}
+
+void constantValue(RankProfile profile, String name) :
+{
+ String value;
+}
+{
+ <COLON> value = identifier() { profile.addConstant(name, Value.parse(value)); }
+}
+
+void constantTensor(RankProfile profile, String name) :
+{
+ String tensorString = "";
+ TensorType tensorType = null;
+}
+{
+ <LBRACE> (<NL>)*
+ (( tensorString = tensorValue() |
+ tensorType = tensorType(profile.getName(), name) ) (<NL>)* )* <RBRACE>
+ {
+ if (tensorType != null) {
+ profile.addConstantTensor(name, new TensorValue(Tensor.from(tensorString), tensorType));
+ } else {
+ profile.addConstantTensor(name, new TensorValue(Tensor.from(tensorString)));
+ }
+ }
+}
+
+String tensorValue() :
+{
+ String tensor;
+}
+{
+ ( <TENSOR_VALUE_SL> { tensor = token.image.substring(token.image.indexOf(":") + 1); } |
+ <TENSOR_VALUE_ML> { tensor = token.image.substring(token.image.indexOf("{") + 1,
+ token.image.lastIndexOf("}")); } )
+ {
+ return tensor;
+ }
+}
+
+TensorType tensorType(String rankProfileName, String constantTensorName) :
+{
+ String tensorTypeString;
+}
+{
+ <TYPE> <COLON> ( <TENSOR_TYPE> { tensorTypeString = token.image; } )
+ {
+ TensorType tensorType;
+ try {
+ tensorType = TensorType.fromSpec(tensorTypeString);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("For constant tensor '" + constantTensorName + "' in rank profile '" + rankProfileName +
+ "': Illegal tensor type spec: " + e.getMessage());
+ }
+ return tensorType;
+ }
+}
+
+/**
+ * This rule consumes an expression token and returns its image.
+ *
+ * @return The consumed token image.
+ */
+String expression() :
+{
+ String exp;
+}
+{
+ ( <EXPRESSION_SL> { exp = token.image.substring(token.image.indexOf(":") + 1); } |
+ <EXPRESSION_ML> { exp = token.image.substring(token.image.indexOf("{") + 1,
+ token.image.lastIndexOf("}")); } )
+ { return exp; }
+}
+
+/**
+ * This rule consumes an identifier. This must be kept in sync with all word tokens that should be parseable as
+ * identifiers.
+ *
+ * @return The identifier string.
+ */
+String identifier() : { }
+{
+ ( <ALIAS>
+ | <ALWAYS>
+ | <ANNOTATION>
+ | <ANNOTATIONREFERENCE>
+ | <ARITY>
+ | <ARRAY>
+ | <ASCENDING>
+ | <ATTRIBUTE>
+ | <BODY>
+ | <BOLDING>
+ | <COMPRESSION>
+ | <COMPRESSIONLEVEL>
+ | <COMPRESSIONTHRESHOLD>
+ | <CONTEXT>
+ | <CREATEIFNONEXISTENT>
+ | <DENSEPOSTINGLISTTHRESHOLD>
+ | <DESCENDING>
+ | <DIRECT>
+ | <DOCUMENT>
+ | <DOCUMENTSUMMARY>
+ | <DOUBLE>
+ | <DYNAMIC>
+ | <ENABLEBITVECTORS>
+ | <ENABLEONLYBITVECTOR>
+ | <EXACT>
+ | <EXACTTERMINATOR>
+ | <FALSE>
+ | <FASTACCESS>
+ | <FASTSEARCH>
+ | <FIELD>
+ | <FIELDS>
+ | <FIELDSET>
+ | <FILE>
+ | <FILTER>
+ | <FIRSTPHASE>
+ | <FULL>
+ | <FUNCTION>
+ | <GRAM>
+ | <HEADER>
+ | <HUGE>
+ | <ID>
+ | <IDENTICAL>
+ | <IDENTIFIER>
+ | <IGNOREDEFAULTRANKFEATURES>
+ | <INDEX>
+ | <INDEXING>
+ | <INDEXINGREWRITE>
+ | <INHERITS>
+ | <INTEGER>
+ | <KEEPRANKCOUNT>
+ | <LITERAL>
+ | <LOCALE>
+ | <LONG>
+ | <LOWERBOUND>
+ | <LOWERCASE>
+ | <MACRO>
+ | <MAP>
+ | <MATCH>
+ | <MATCHPHASE>
+ | <MAXFILTERCOVERAGE>
+ | <MAXHITS>
+ | <MTOKEN>
+ | <NEVER>
+ | <NONE>
+ | <NOPREFETCH>
+ | <NORMAL>
+ | <NORMALIZING>
+ | <OFF>
+ | <ON>
+ | <ONDEMAND>
+ | <ORDER>
+ | <PREFETCH>
+ | <PREFIX>
+ | <PRIMARY>
+ | <PROPERTIES>
+ | <QUATERNARY>
+ | <QUERYCOMMAND>
+ | <RANK>
+ | <RANKPROFILE>
+ | <RANKPROPERTIES>
+ | <RANKSCOREDROPLIMIT>
+ | <RANKTYPE>
+ | <RAW>
+ | <REMOVEIFZERO>
+ | <RERANKCOUNT>
+ | <RISE>
+ | <SEARCH>
+ | <SECONDARY>
+ | <SECONDPHASE>
+ | <SORTING>
+ | <SOURCE>
+ | <SSCONTEXTUAL>
+ | <SSOVERRIDE>
+ | <SSTITLE>
+ | <SSURL>
+ | <STATIC>
+ | <STEMMING>
+ | <STRENGTH>
+ | <STRING>
+ | <STRUCT>
+ | <SUBSTRING>
+ | <SUFFIX>
+ | <SUMMARY>
+ | <SUMMARYTO>
+ | <SYMMETRIC>
+ | <TERTIARY>
+ | <TEXT>
+ | <TO>
+ | <TRUE>
+ | <TYPE>
+ | <UCA>
+ | <UPPERBOUND>
+ | <USEDOCUMENT>
+ | <VARIABLE>
+ | <WEIGHT>
+ | <WEIGHTEDSET>
+ | <WORD>
+ | <INLINE>
+ | <CONSTANTS>
+ )
+ { return token.image; }
+}
+
+/**
+ * This rule consumes a string token and returns the token image.
+ *
+ * @return The consumed token image.
+ */
+String string() : { }
+{
+ <STRING> { return token.image; }
+}
+
+/**
+ * This rule consumes a quoted string token and returns the token image minus the quotes. This does not perform
+ * unescaping of the content, it simply removes the first and last character of the image. However, the token itself can
+ * contain anything but a double quote.
+ *
+ * @return The unquoted token image.
+ */
+String quotedString() : { }
+{
+ <QUOTEDSTRING> { return token.image.substring(1, token.image.length() - 1); }
+}
+
+/**
+ * This rule consumes a boolean value.
+ *
+ * @return The consumed boolean value.
+ */
+Boolean bool() : { }
+{
+ ( ( <ON> | <TRUE> ) { return true; } |
+ ( <OFF> | <FALSE> ) { return false; } )
+}
+
+/**
+ * This rule consumes an integer token and returns its numeric value.
+ *
+ * @return The consumed integer value.
+ */
+int integer() : { }
+{
+ <INTEGER> { return Integer.parseInt(token.image); }
+}
+
+/**
+ * This rule consumes a long or integer token and returns its numeric value.
+ *
+ * @return The consumed long value.
+ */
+long consumeLong() : { }
+{
+ ( <INTEGER> { return Long.parseLong(token.image); } |
+ <LONG> { return Long.parseLong(token.image.substring(0, token.image.length()-1)); }
+ )
+}
+
+/**
+ * This rule consumes a floating-point token and returns its numeric value.
+ *
+ * @return The consumed value.
+ */
+double consumeFloat() : { }
+{
+ <DOUBLE> { return Double.valueOf(token.image); }
+}
+
+/**
+ * This rule consumes an opening brace with leading and trailing newline tokens.
+ */
+void lbrace() : { }
+{
+ (<NL>)* <LBRACE> (<NL>)*
+}
diff --git a/config-model/src/main/perl/deploy b/config-model/src/main/perl/deploy
new file mode 100755
index 00000000000..00e3b0d91f8
--- /dev/null
+++ b/config-model/src/main/perl/deploy
@@ -0,0 +1,614 @@
+#!/usr/local/bin/perl -w
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# This script is for uploading, preparing, activating and fetching
+# application packages to a cloud config server
+
+# BEGIN perl environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+use File::Basename;
+use File::Path;
+
+sub findpath {
+ my $myfullname = ${0};
+ my($myname, $mypath) = fileparse($myfullname);
+
+ return $mypath if ( $mypath && -d $mypath );
+ $mypath=`pwd`;
+
+ my $pwdfullname = $mypath . "/" . $myname;
+ return $mypath if ( -f $pwdfullname );
+ return 0;
+}
+
+# Returns the argument path if it seems to point to VESPA_HOME, 0 otherwise
+sub is_vespa_home {
+ my($VESPA_HOME) = shift;
+ my $COMMON_ENV="libexec/vespa/common-env.sh";
+ if ( $VESPA_HOME && -d $VESPA_HOME ) {
+ my $common_env = $VESPA_HOME . "/" . $COMMON_ENV;
+ return $VESPA_HOME if -f $common_env;
+ }
+ return 0;
+}
+
+# Returns the home of Vespa, or dies if it cannot
+sub findhome {
+ # Try the VESPA_HOME env variable
+ return $ENV{'VESPA_HOME'} if is_vespa_home($ENV{'VESPA_HOME'});
+ if ( $ENV{'VESPA_HOME'} ) { # was set, but not correctly
+ die "FATAL: bad VESPA_HOME value '" . $ENV{'VESPA_HOME'} . "'\n";
+ }
+
+ # Try the ROOT env variable
+ $ROOT = $ENV{'ROOT'};
+ return $ROOT if is_vespa_home($ROOT);
+
+ # Try the script location or current dir
+ my $mypath = findpath();
+ if ($mypath) {
+ while ( $mypath =~ s|/[^/]*$|| ) {
+ return $mypath if is_vespa_home($mypath);
+ }
+ }
+ die "FATAL: Missing VESPA_HOME environment variable\n";
+}
+
+BEGIN {
+ my $tmp = findhome();
+ if ( $tmp !~ m{[/]$} ) { $tmp .= "/"; }
+ $ENV{'VESPA_HOME'} = $tmp;
+}
+my $VESPA_HOME = $ENV{'VESPA_HOME'};
+
+# END perl environment bootstrap section
+
+use lib $ENV{'VESPA_HOME'} . '/lib/perl5/site_perl';
+use Yahoo::Vespa::Defaults;
+readConfFile();
+
+use strict;
+use warnings;
+use feature qw(switch say);
+use vars qw/ $opt_h $opt_n $opt_v $opt_f $opt_t $opt_1 $opt_a $opt_e $opt_E $opt_r $opt_i $opt_p $opt_H $opt_R /;
+use Env qw($HOME);
+use JSON;
+use Getopt::Std;
+use File::Path qw(make_path);
+use Scalar::Util qw(looks_like_number);
+
+my $cloudconfig_dir = "$HOME/.cloudconfig";
+my $session_id_file;
+my $configsource_url_used_file = "$cloudconfig_dir/deploy-configsource-url-used";
+
+my $curl_command = 'curl --silent --show-error --connect-timeout 30 --max-time 300';
+
+my $CURL_PUT = $curl_command . ' --write-out \%{http_code} --request PUT';
+my $CURL_GET = $curl_command . ' --request GET';
+my $GZIP = "gzip";
+my $CURL_POST_WITH_HEADERS = $curl_command . ' -i --request POST --header "Content-Type: application/x-gzip" --data-binary @- -D /tmp/http-headers';
+my $CURL_POST = $curl_command . ' --write-out \%{http_code} --request POST --header "Content-Type: application/x-gzip" --data-binary @-';
+my $CURL_POST_ZIP = $curl_command . ' --write-out \%{http_code} --request POST --header "Content-Type: application/zip" --data-binary @-';
+
+my $pathPrefix;
+my $tenant = "default";
+my $application = "default";
+my $environment = "prod";
+my $region = "default";
+my $instance = "default";
+my $version = "v2";
+my $port = "19071";
+
+getopts('fhnt:v1e:E:r:a:i:p:HR:');
+
+if ($opt_h) {
+ usage();
+ exit 0;
+}
+
+if ($opt_e) {
+ $tenant = $opt_e;
+}
+
+if ($opt_r) {
+ $region = $opt_r;
+}
+
+if ($opt_E) {
+ $environment = $opt_E;
+}
+
+if ($opt_a) {
+ $application = $opt_a;
+}
+
+if ($opt_i) {
+ $instance = $opt_i;
+}
+
+if ($opt_p) {
+ $port = $opt_p;
+}
+
+# Use v1 of application API
+if ($opt_1) {
+ $version = "v1";
+ $pathPrefix = "/application/v1/session";
+} else {
+ $pathPrefix = "/application/v2/tenant/$tenant/session";
+}
+
+create_cloudconfig_dir();
+
+$session_id_file = "$cloudconfig_dir/$tenant/deploy-session-id";
+
+my $command = shift;
+
+
+given($command) {
+ when ("upload") {
+ my $application_package = shift;
+ if (!$application_package) {
+ print "Command failed. No application package specified\n";
+ usage("upload");
+ exit 1;
+ }
+ if (!(-e $application_package)) {
+ print "Command failed. No such directory found: '$application_package'\n";
+ exit 1;
+ }
+ check_application_directory($application_package);
+
+ do_http_request("upload", $application_package);
+ }
+ when ("prepare") {
+ my $arg = shift;
+ if ($arg && looks_like_number($arg) && !(-d $arg)) {
+ do_http_request("prepare", "", $arg);
+ } elsif ($arg) {
+ check_application_directory($arg);
+ do_http_request("upload", $arg);
+ do_http_request("prepare");
+ } else {
+ do_http_request("prepare");
+ }
+ }
+ when ("activate") {
+ my $session_id = shift;
+ do_http_request("activate", "", $session_id);
+ }
+ when ("fetch") {
+ my $arg = shift;
+ if ($arg) {
+ fetch_active_application($arg);
+ } else {
+ usage("fetch", $arg);
+ }
+ }
+ when ("help") {
+ my $arg = shift;
+ usage($command, $arg);
+ }
+ default { usage($command); }
+}
+
+
+sub check_application_directory {
+ my ($application_package) = shift;
+
+ if (-d $application_package) {
+ # OK
+ } elsif ((-f $application_package) && ($application_package =~ /.*\.zip/ )) {
+ # OK
+ } else {
+ print "Command failed. No directory or zip file found: '$application_package'\n";
+ exit 1;
+ }
+}
+
+sub usage {
+ my ($command, $arg) = @_;
+
+ if ($command && $command eq "help") {
+ $command = $arg;
+ }
+
+ given($command) {
+ when ("upload") { usage_upload(); }
+ when ("prepare") { usage_prepare(); }
+ when ("activate") { usage_activate(); }
+ when ("fetch") { usage_fetch(); }
+ default {
+ print "Usage: deploy [-h] [-v] [-f] [-t] [-p] [<command>] [args]\n";
+ print "Supported commands: 'upload', 'prepare', 'activate', 'fetch' and 'help'\n";
+ print "Supported options: '-h' (help), '-v' (verbose), '-f' (force/ignore validation errors), '-t' (timeout in seconds), '-p' (config server http port)\n";
+ print "Try 'deploy help <command>' to get more help\n";
+ }
+ }
+}
+
+sub usage_upload {
+ print "Usage: deploy upload <application package>\n";
+}
+
+sub usage_prepare {
+ print "Usage: deploy prepare [<session_id> | <application package>]\n";
+}
+
+sub usage_activate {
+ print "Usage: deploy activate [<session_id>]\n";
+}
+
+sub usage_fetch {
+ print "Usage: deploy fetch <output directory>\n";
+}
+
+sub fetch_active_application {
+ my ($outputdir) = @_;
+ my ($configsource_url, @configsources) = get_configsource_url("fetch");
+ my $url = "$configsource_url$pathPrefix/active/content/";
+ if ($version eq "v2") {
+ $url = $configsource_url
+ . "/application/v2"
+ . "/tenant/${tenant}"
+ . "/application/${application}"
+ . "/environment/${environment}"
+ . "/region/${region}"
+ . "/instance/${instance}"
+ . "/content/";
+ }
+ my $output = http_content($url);
+ my $exitcode = $? >> 8;
+ if ($exitcode != 0) {
+ print_request_failed($exitcode, $configsource_url);
+ exit 1;
+ } else {
+ print "Writing active application to $outputdir\n";
+ `mkdir -p $outputdir`;
+ die "$outputdir is not writeable. Please check permissions\n" if (! -w $outputdir);
+ my $json_text = get_json($output);
+ if(ref($json_text) eq 'ARRAY'){
+ fetch_directory($json_text, $outputdir);
+ } else {
+ print "Error response: $json_text->{message}\n";
+ exit 1;
+ }
+ }
+}
+
+sub fetch_directory {
+ my ($json, $outputdir) = @_;
+ `mkdir -p $outputdir`;
+ foreach my $entry (@{$json}) {
+ my $name = "$outputdir/";
+ if ($entry =~ /\/([^\/]+\/?)$/) {
+ $name .= $1;
+ }
+ if ($name =~ /(.*)\/$/) {
+ my $dir = $1;
+ my $output = http_content($entry);
+ my $json_text = get_json($output);
+ fetch_directory($json_text, "$dir");
+ } else {
+ my $output = http_content($entry);
+ open(FH, ">$name");
+ print FH $output;
+ close(FH);
+ }
+ }
+}
+
+sub get_configsource_url {
+ my ($command) = @_;
+
+ my @configsources = split(' ', `$VESPA_HOME/libexec/vespa/vespa-config.pl -confighttpsources`);
+ my $configsource_url = shift(@configsources);
+ if (!$configsource_url) {
+ die "Could not get url to config server, make sure that VESPA_HOME and services.addr_configserver is set\n";
+ }
+ chomp($configsource_url);
+ my @temp = split(':', $configsource_url, 3);
+ $configsource_url = $temp[0] . ":" . $temp[1] . ":" . $port;
+ if (!$configsource_url) {
+ print "Could not get url to config server, make sure that services.addr_configserver is set\n";
+ exit 1;
+ }
+
+ # configsource_url to be used by prepare and activate
+ if ($command eq "prepare" || $command eq "activate") {
+ my $temp = get_configsource_url_used();
+ if ($temp and $temp ne "") {
+ $configsource_url = $temp;
+ debug("Using config server URL " . $configsource_url . " read from file\n");
+ } else {
+ print "Could not read config server URL used for previous upload of an application package, trying to use $configsource_url\n";
+ }
+ }
+ return ($configsource_url, @configsources);
+}
+
+sub do_http_request {
+ my ($command, $application_package, $supplied_session_id) = @_;
+ my ($configsource_url, @configsources) = get_configsource_url($command);
+
+ my $output;
+ my $exitcode = 1;
+ given($command) {
+ when ("upload") {
+ ($exitcode, $output) = http_upload(\@configsources, $configsource_url, $application_package);
+ }
+ when ("prepare") {
+ $output = http_prepare($configsource_url, $supplied_session_id);
+ $exitcode = $? >> 8;
+ }
+ when ("activate") {
+ $output = http_activate($configsource_url, $supplied_session_id);
+ $exitcode = $? >> 8;
+ }
+ }
+
+ my $response;
+ if ($exitcode != 0) {
+ print_request_failed($exitcode, $configsource_url);
+ exit 1;
+ } else {
+ my $status_code;
+ ($status_code, $response) = parse_http_response($output);
+ if ($status_code != 200) {
+ print "Request failed. HTTP status code: $status_code\n";
+ print_response($response);
+ exit 1;
+ }
+ print_response($response);
+ }
+}
+
+sub http_upload {
+ my ($temp, $configsource_url, $application_package) = @_;
+ my @configsources = @{$temp};
+
+ my $output;
+ my $exitcode = 0;
+ my $retry = 0;
+ my $configsource_url_used = $configsource_url;
+ LOOP: {
+ do {
+ $output = http_upload_lowlevel($configsource_url, $application_package);
+ $exitcode = $? >> 8;
+ last LOOP if ($exitcode == 0);
+
+ debug("exitcode=$exitcode\n");
+ debug("output=$output\n");
+ $configsource_url = shift(@configsources);
+ if ($configsource_url) {
+ $configsource_url_used = $configsource_url;
+ $retry = 1;
+ print_request_failed($exitcode, $configsource_url_used);
+ print "Retrying with another config server\n";
+ } else {
+ print_request_failed($exitcode, $configsource_url_used);
+ exit 1;
+ }
+ } while ($retry);
+ }
+
+ write_session_id($output);
+ write_configsource_url_used($configsource_url_used);
+
+ return ($exitcode, $output);
+}
+
+sub http_upload_lowlevel {
+ my ($source, $app) = @_;
+
+ my $TAR="tar -C $app --dereference --exclude='.[a-zA-Z0-9]*' --exclude=ext -cf - . --transform=\"s#^#application/#\" ";
+ my $url = $source . $pathPrefix;
+ my $appPath = `readlink -m $app`;
+ my $appName = `basename $appPath`;
+ chomp($appName);
+ $url = add_url_property_from_flag($url, $opt_v, "verbose");
+ $url = add_url_property($url, "name=$appName");
+ print "Uploading application '$app' using $url\n";
+ if (-f $app) {
+ `cat $app | $CURL_POST_ZIP $url`;
+ } else {
+ `$TAR | $GZIP | $CURL_POST $url`;
+ }
+}
+
+sub http_prepare {
+ my $source = shift;
+ my $session_id = shift || get_session_id();
+
+ my $url = $source . $pathPrefix . "/$session_id/prepared";
+ $url = add_url_property_from_flag($url, $opt_f, "ignoreValidationErrors");
+ $url = add_url_property_from_flag($url, $opt_n, "dryrun");
+ $url = add_url_property_from_flag($url, $opt_v, "verbose");
+ $url = add_url_property_from_flag($url, $opt_H, "hostedVespa");
+ $url = add_url_property_from_option($url, $opt_a, "applicationName");
+ $url = add_url_property_from_option($url, $opt_i, "instance");
+ $url = add_url_property_from_option($url, $opt_t, "timeout");
+ $url = add_url_property_from_option($url, $opt_R, "rotations");
+ print "Preparing session $session_id using $url\n";
+ `$CURL_PUT \"$url\"`;
+}
+
+sub http_content {
+ my $url = shift;
+ print "Getting content using $url\n";
+ `$CURL_GET \"$url\"`;
+}
+
+sub http_activate {
+ my $source = shift;
+ my $session_id = shift || get_session_id();
+
+ my $url = $source . $pathPrefix . "/$session_id/active";
+ $url = add_url_property_from_flag($url, $opt_v, "verbose");
+ $url = add_url_property_from_option($url, $opt_t, "timeout");
+ print "Activating session $session_id using $url\n";
+ `$CURL_PUT \"$url\"`;
+}
+
+sub get_session_id {
+ my $session_id = `cat $session_id_file 2>/dev/null`;
+ unless ($session_id) {
+ print "Could not read session id from file, and no session id supplied as argument. Exiting.\n";
+ exit 1
+ }
+ $session_id;
+}
+
+sub get_session_id_from_response {
+ my ($response) = @_;
+
+ my $new_session_id;
+ if ($response =~ /.*"session-id":"(\d+)".*/) { $new_session_id = $1; }
+ $new_session_id;
+}
+
+sub print_response {
+ my ($response) = @_;
+ chomp($response);
+ debug("$response\n");
+ if ($response) {
+ my $json_text = get_json($response);
+ my $error = $json_text->{error};
+ if ($error) {
+ print "$json_text->{error}\n";
+ }
+ my $status = $json_text->{status};
+ foreach my $log_message (@{$json_text->{log}}) {
+ print "$log_message->{level}: $log_message->{message}\n";
+ }
+ my $message = $json_text->{message};
+ if ($message) {
+ print "$message\n";
+ }
+ my $metadata_deploy = $json_text->{deploy};
+ if ($metadata_deploy) {
+ my $timestamp = $metadata_deploy->{timestamp};
+ my $metadata_application = $json_text->{application};
+ my $checksum = $metadata_application->{checksum};
+ my $generation = $metadata_application->{generation};
+ print "Checksum: $checksum\n";
+ print "Timestamp: $timestamp\n";
+ print "Generation: $generation\n";
+ }
+ } else {
+ print "Empty response";
+ }
+}
+
+sub get_json {
+ my ($response) = @_;
+
+ my $json_dir = JSON->new;
+ return $json_dir->utf8->decode($response);
+}
+
+# extend $url with $url_property=true if $flag is set
+sub add_url_property_from_flag {
+ my ($url, $flag, $url_property) = @_;
+ return $url unless $flag;
+ return add_url_property($url, "$url_property=true");
+}
+
+# extend $url with $url_property=$opt if $opt is set
+sub add_url_property_from_option {
+ my ($url, $opt, $url_property) = @_;
+ return $url unless $opt;
+ add_url_property($url, "$url_property=$opt");
+}
+
+sub add_url_property {
+ my ($url, $url_property) = @_;
+ if ($url =~ /\?/) {
+ $url = $url . "&" . $url_property;
+ } else {
+ $url = $url . "?" . $url_property;
+ }
+ $url;
+}
+
+sub write_session_id {
+ my ($response) = @_;
+
+ my $new_session_id = get_session_id_from_response($response);
+ if ($new_session_id) {
+ open(my $fh, '>', $session_id_file) or die "Could not open file '$session_id_file' $!";
+ print $fh $new_session_id;
+ close $fh;
+ }
+}
+
+sub write_configsource_url_used {
+ my ($configsource_url) = @_;
+
+ if ($configsource_url) {
+ open(my $fh2, '>', $configsource_url_used_file) or die "Could not open file '$configsource_url_used_file' $!";
+ print $fh2 $configsource_url;
+ close $fh2;
+ }
+}
+
+sub get_configsource_url_used {
+ my $configsource_url = `cat $configsource_url_used_file 2>/dev/null` || "";
+ $configsource_url;
+}
+
+sub print_request_failed {
+ my ($exitcode, $configsource_url) = @_;
+
+ my $message = "HTTP request failed";
+ if ($exitcode == 7) {
+ $message .= ". Could not connect to $configsource_url";
+ } else {
+ $message .= " with curl exit code $exitcode";
+ }
+
+ print $message . "\n";
+}
+
+
+sub debug {
+ my ($message) = @_;
+
+ if ($opt_v) {
+ print "$message";
+ }
+}
+
+sub parse_http_response {
+ my ($response) = @_;
+
+ my $message = "";
+ my $status_code = 500;
+
+ if ($response =~ /(.*)(\d\d\d)/) {
+ $message = $1;
+ $status_code = int($2);
+ }
+ return ($status_code, $message);
+}
+
+sub create_cloudconfig_dir {
+ my $path = "$cloudconfig_dir/$tenant";
+ if (-e $path) {
+ check_dir_permissions($path);
+ } else {
+ make_path($path);
+ }
+}
+
+
+sub check_dir_permissions {
+ my ($dir) = @_;
+
+ if (!(-d $dir)) {
+ print "$dir is not a directory, please fix\n";
+ }
+ if (!(-r $dir)) {
+ print "$dir is not readable, please fix\n";
+ }
+}
diff --git a/config-model/src/main/perl/deploy-application b/config-model/src/main/perl/deploy-application
new file mode 100755
index 00000000000..3506aa3c453
--- /dev/null
+++ b/config-model/src/main/perl/deploy-application
@@ -0,0 +1,77 @@
+#!/usr/bin/env perl
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# This script is a wrapper exposing the legacy deploy-application script
+
+use strict;
+use warnings;
+use Getopt::Long;
+
+my $help=0;
+my $debug=0;
+my $dryrun=0;
+my $force=0;
+my $nobackup=0;
+my $skip_xml_validation=0;
+my $verbose=0;
+my $nosudo=0;
+my $write=0;
+my $outputdir=0;
+
+GetOptions ("h" => \$help,
+ "d" => \$debug,
+ "n" => \$dryrun,
+ "f" => \$force,
+ "nobackup" => \$nobackup,
+ "skipxmlvalidation" => \$force,
+ "v" => \$verbose,
+ "nosudo" => \$nosudo,
+ "w" => \$write,
+ "p" => \$outputdir)
+or $help = 1;
+
+if ($help) {
+ print_usage();
+ exit(0);
+}
+
+my $app = shift;
+
+my $flags = " ";
+if ($debug) {
+ $flags .= "-v "
+}
+if ($force) {
+ $flags .= "-f ";
+}
+if ($verbose) {
+ $flags .= "-v ";
+}
+if ($dryrun) {
+ $flags .= "-n ";
+}
+
+my $DEPLOY="deploy $flags prepare $app";
+
+system($DEPLOY);
+if ($? == 0) { print "Activate the application by running 'activate-application'\n"; }
+
+sub print_usage {
+ print "\n";
+ print "usage: deploy-application <application-directory>\n";
+ print "Switches:\n";
+ print "-h This help text\n";
+ print "-d Debug (stack trace on exceptions)\n";
+ print "-n Dry-run, check application without activating, print the MD5 checksum of the application\n";
+# print "-nobackup Do not backup application package data\n";
+# print "-skipxmlvalidation Skip validation of application package XML files\n";
+ print "-v Verbose (progress trace)\n";
+# print "-nosudo Do not sudo as yahoo when deploying\n";
+# print "-w Write .MODEL debugging files when the vespa model is built\n";
+ print "-f Force, try to deploy even if package contains errors\n";
+# print "-p <Directory to be used for writing (if using -w) and reading config system files during deploy. Default: $VESPA_HOME/var/db/vespa/config_server/serverdb>\n";
+ print "\n";
+ print "Verify that the 'yahoo' user has read and execute permissions\n";
+ print "to each level of the directory tree from the current directory\n";
+ print "down to and including the application directory.\n";
+}
+
diff --git a/config-model/src/main/perl/expand-config.pl b/config-model/src/main/perl/expand-config.pl
new file mode 100755
index 00000000000..643de484620
--- /dev/null
+++ b/config-model/src/main/perl/expand-config.pl
@@ -0,0 +1,84 @@
+#!/usr/local/bin/perl
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#============================================================================
+# @version $Id: expand-config.pl,v 1.1 2006-07-26 15:52:43 gv Exp $
+# @project Vespa Admin
+# @author Gj�ran Voldengen
+# @date created 2005-04-15
+#
+# Create a vespa config file from an application package config file
+# that might contain "file=<filename>" statements. The output stream
+# consists of the original file contents, with the expanded and escaped
+# contents of the files given in file= statements.
+#=============================================================================
+
+use strict;
+
+$| = 1;
+
+# Check for correct number of command line args
+if ( int(@ARGV) != 0 ) {
+ die ("\nUsage: cat infile | $0 > outfile\n\n");
+}
+
+
+#============================================================================
+# Global Constants
+#============================================================================
+
+# "Reserved keywords" to recognize in input file
+my $FILE = "file";
+
+
+#============================================================================
+# Subroutines
+#============================================================================
+
+#---------------------------------------------------------------------------
+# Expand the contents of the input file into a one-line string,
+# escaping special chars.
+#---------------------------------------------------------------------------
+sub expandFile {
+ my ($filename) = @_;
+
+ my $config = "";
+
+ # Read the complete input file into a single string
+ open (INFILE, "$filename") || die "Cannot open $filename\n";
+ while (<INFILE>) {
+ $config .= $_;
+ }
+
+ $config =~ s{\\}{\\\\}g;
+ $config =~ s{\"}{\\\"}g; #" emacs gets confused..
+ $config =~ s{\n}{\\n}g;
+
+ return $config;
+}
+
+
+#============================================================================
+# Main program
+#============================================================================
+my $file = "";
+
+while (<STDIN>) {
+
+ # Comment lines are allowed, and must be preserved, along with all
+ # lines that don't contain "file="
+ unless (m/^\#/) {
+ # Allow several files on one line
+ while (m{$FILE \s* = \s* ([^\s\"]+) }x) { #"
+ $file = $1;
+ $file =~ s{^\s+}{};
+ $file =~ s{\s+ $ }{}x;
+ $_ = $` . expandFile($file) . $';
+ }
+ }
+ print STDOUT ($_);
+}
+
+
+
+############################### File end ################################
diff --git a/config-model/src/main/perl/vespa-replicate-log-stream b/config-model/src/main/perl/vespa-replicate-log-stream
new file mode 100755
index 00000000000..3e02b498554
--- /dev/null
+++ b/config-model/src/main/perl/vespa-replicate-log-stream
@@ -0,0 +1,37 @@
+#!/usr/local/bin/perl
+
+use IO::Socket::INET;
+
+my $cmd = "vespa-model-inspect -t logtp -t telnet -t replicator filter:ports";
+
+print STDERR "Looking for logserver port ($cmd)\n";
+
+my $vmiout = `$cmd`;
+my $host = "";
+my $port = "";
+
+if ( $vmiout =~ m{^\s*tcp/(\S+):(\d+)\s*$/} ) {
+ $host = $1;
+ $port = $2;
+ print STDERR "following logserver at '${host}:${port}'\n";
+} else {
+ die "FAILED finding host and port for logserver replication\n";
+}
+
+my $net = new IO::Socket::INET(PeerAddr => "${host}:${port}",
+ Proto => 'tcp',
+ Timeout => 10);
+if (!$net) {
+ die "could not contact logserver at ${host}:${port}\n";
+}
+
+print $net "use system.all\r\n";
+
+while (<$net>) {
+ if (m/^#/) {
+ print STDERR;
+ } else {
+ print;
+ }
+}
+exit(0);
diff --git a/config-model/src/main/resources/schema/.gitignore b/config-model/src/main/resources/schema/.gitignore
new file mode 100644
index 00000000000..b60c856f91a
--- /dev/null
+++ b/config-model/src/main/resources/schema/.gitignore
@@ -0,0 +1,2 @@
+*.rng
+*.xsd
diff --git a/config-model/src/main/resources/schema/admin.rnc b/config-model/src/main/resources/schema/admin.rnc
new file mode 100644
index 00000000000..36af33d06cb
--- /dev/null
+++ b/config-model/src/main/resources/schema/admin.rnc
@@ -0,0 +1,95 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+Admin = AdminV2 | AdminV3 | AdminV4
+
+AdminV2 =
+ element admin {
+ attribute version { "2.0" } &
+ element adminserver { service.attlist } &
+ GenericConfig* &
+ LogServer? &
+ (ConfigServer | ConfigServers)? &
+ FileDistribution? &
+ AdminSlobroks? &
+ AdminYamas? &
+ MetricConsumers? &
+ ClusterControllers?
+ }
+
+AdminV3 =
+ element admin {
+ attribute version { "3.0" } &
+ Nodes
+ }
+
+AdminV4 =
+ element admin {
+ attribute version { "4.0" } &
+ AdminV4Slobroks? &
+ AdminV4LogServers?
+ }
+
+AdminV4Slobroks =
+ element slobroks {
+ OptionalDedicatedNodes
+ }
+
+AdminV4LogServers =
+ element logservers {
+ OptionalDedicatedNodes
+ }
+
+AdminSlobroks =
+ element slobroks {
+ element slobrok {
+ service.attlist &
+ attribute index { xsd:nonNegativeInteger }?
+ }+
+ }
+
+AdminYmon =
+ element ymon {
+ attribute endpoint { xsd:anyURI { minLength = "1" } }?,
+ attribute clustername { xsd:NCName }?,
+ attribute interval { xsd:int }?
+ }
+
+AdminYamas =
+ element yamas {
+ attribute interval { xsd:int }?,
+ attribute systemname { xsd:string }?
+ }
+
+
+ConfigServer = element configserver {
+ service.attlist
+}
+
+ConfigServers = element configservers {
+ ConfigServer+
+}
+
+LogServer = element logserver {
+ service.attlist
+}
+
+FileDistribution = element filedistribution {
+ element uploadbitrate { xsd:string { pattern = "\d+(\.\d*)?\s*[kmgKMG]?" } }? &
+ element downloadbitrate { xsd:string { pattern = "\d+(\.\d*)?\s*[kmgKMG]?" } }?
+}
+
+MetricConsumers = element metric-consumers {
+ element consumer {
+ attribute name { xsd:string { pattern = "[a-zA-Z][_a-zA-Z0-9]*" } }&
+ element metric {
+ attribute name{xsd:Name} &
+ attribute output-name{xsd:Name}?
+ }+
+ }+
+}
+
+ClusterControllers = element cluster-controllers {
+ attribute standalone-zookeeper { xsd:string }? &
+ element cluster-controller {
+ service.attlist
+ }+
+}
diff --git a/config-model/src/main/resources/schema/clients-v2.rnc b/config-model/src/main/resources/schema/clients-v2.rnc
new file mode 100644
index 00000000000..b53b6e0a191
--- /dev/null
+++ b/config-model/src/main/resources/schema/clients-v2.rnc
@@ -0,0 +1,64 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+Clients20 =
+ attribute version { "2.0" } &
+ LoadTypes? &
+ Spoolers20?
+
+LoadTypes = element load-types {
+ element type {
+ attribute name { text } &
+ attribute default-priority { text }?
+ }*
+}
+
+Spoolers20 = element spoolers {
+ attribute jvmargs { text }? &
+ SpoolMaster20* &
+ FeederOptions20? &
+ Spooler20+
+}
+
+Spooler20 = element spooler {
+ service.attlist? &
+ attribute id { xsd:string }? &
+ attribute directory { xsd:string }? &
+ attribute keepsuccess { xsd:boolean }? &
+ attribute maxfailuresize { xsd:integer }? &
+ attribute maxfatalfailuresize { xsd:integer }? &
+ attribute threads { xsd:integer }? &
+ attribute maxretries { xsd:integer }? &
+ FeederOptionsOpts20? &
+ element parsers {
+ element parser {
+ attribute type { text } &
+ element parameter {
+ attribute key { xsd:string { minLength = "1" } } &
+ attribute value { xsd:string { minLength = "1" } }
+ }*
+ }+
+ }?
+}
+
+SpoolMaster20 = element spoolmaster {
+ service.attlist?
+}
+
+FeederOptions20 = element feederoptions {
+ FeederOptionsOpts20 &
+ DocProcChain?
+}
+
+FeederOptionsOpts20 = element abortondocumenterror { xsd:boolean }? &
+ element retryenabled { text }? &
+ element route { text }? &
+ element maxpendingdocs { xsd:positiveInteger }? &
+ element maxpendingbytes { xsd:positiveInteger }? &
+ element retrydelay { xsd:double { minInclusive = "0.0" } }? &
+ element timeout { xsd:double { minInclusive = "0.0" } }? &
+ element tracelevel { xsd:positiveInteger }? &
+ element mbusport { xsd:positiveInteger }?
+
+
+DocProcChain = element docprocchain {
+ text
+}
diff --git a/config-model/src/main/resources/schema/clients.rnc b/config-model/src/main/resources/schema/clients.rnc
new file mode 100644
index 00000000000..0140fc2f9ce
--- /dev/null
+++ b/config-model/src/main/resources/schema/clients.rnc
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+include "clients-v2.rnc"
+
+Clients = element clients {
+ Clients20
+}
diff --git a/config-model/src/main/resources/schema/common.rnc b/config-model/src/main/resources/schema/common.rnc
new file mode 100644
index 00000000000..06e7b945c18
--- /dev/null
+++ b/config-model/src/main/resources/schema/common.rnc
@@ -0,0 +1,58 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+service.attlist &= attribute hostalias { xsd:NCName }
+service.attlist &= attribute baseport { xsd:unsignedShort }?
+service.attlist &= attribute jvmargs { text }?
+# preload is for internal use only
+service.attlist &= attribute preload { text }?
+
+anyElement = element * {
+ (attribute * { text }
+ | text
+ | anyElement)*
+}
+
+# Valid qualified java class name. See http://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.8
+JavaId = xsd:string { pattern = "([a-zA-Z_$][a-zA-Z\d_$]*\.)*[a-zA-Z_$][a-zA-Z\d_$]*" }
+
+Nodes = element nodes {
+ attribute count { xsd:positiveInteger } &
+ attribute flavor { xsd:string }? &
+ attribute docker-image { xsd:string }?
+}
+
+OptionalDedicatedNodes = element nodes {
+ attribute count { xsd:positiveInteger } &
+ attribute flavor { xsd:string }? &
+ attribute docker-image { xsd:string }? &
+ attribute dedicated { xsd:boolean }?
+}
+
+GenericConfig = element config {
+ attribute name { text },
+ attribute namespace { text }?,
+ attribute version { text }?,
+ anyElement +
+}
+
+ComponentSpec =
+ ( attribute id { xsd:Name | JavaId } | attribute idref { xsd:Name } | attribute ident { xsd:Name } )
+
+ComponentId =
+ ComponentSpec
+
+BundleSpec =
+ attribute class { xsd:Name | JavaId }? &
+ attribute bundle { xsd:Name }?
+
+Component = element component {
+ ComponentDefinition
+}
+
+ComponentDefinition =
+ ComponentId &
+ BundleSpec &
+ GenericConfig* &
+ Component*
+
+
+
diff --git a/config-model/src/main/resources/schema/container-include.rnc b/config-model/src/main/resources/schema/container-include.rnc
new file mode 100644
index 00000000000..71f01e93aac
--- /dev/null
+++ b/config-model/src/main/resources/schema/container-include.rnc
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+include "containercluster.rnc"
+include "common.rnc"
+include "container.rnc"
+include "docproc.rnc"
+include "searchchains.rnc"
+
+start = SearchInContainer | DocprocInContainer | ProcessingInContainer | Components
diff --git a/config-model/src/main/resources/schema/container.rnc b/config-model/src/main/resources/schema/container.rnc
new file mode 100644
index 00000000000..7168e05ba0b
--- /dev/null
+++ b/config-model/src/main/resources/schema/container.rnc
@@ -0,0 +1,41 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#Schema for common container options
+
+include "processing.rnc"
+
+Handler = element handler {
+ ComponentDefinition &
+ ServerBindings &
+ element clientBinding {text}*
+}
+
+Binding = element binding {text}
+ServerBindings = Binding*
+
+Client = element client {
+ ComponentDefinition &
+ element binding {text}* &
+ element serverBinding {text}*
+}
+
+Server = element server {
+ ComponentDefinition
+}
+
+HttpFilterChain =
+ HttpFilter* &
+ ChainBaseContent &
+ ServerBindings
+
+HttpFilter = element filter {
+ ComponentDefinition &
+ FilterConfig?
+}
+
+FilterConfig = element filter-config {
+ anyElement*
+}
+
+Renderer = element renderer {
+ ComponentDefinition
+}
diff --git a/config-model/src/main/resources/schema/containercluster.rnc b/config-model/src/main/resources/schema/containercluster.rnc
new file mode 100644
index 00000000000..3026c386fd4
--- /dev/null
+++ b/config-model/src/main/resources/schema/containercluster.rnc
@@ -0,0 +1,217 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ContainerCluster = element container | jdisc {
+ attribute version { "1.0" } &
+ attribute id { xsd:NCName }? &
+ attribute jetty { xsd:boolean }? &
+ Include* &
+ ContainerServices &
+ DocumentBinding* &
+ Aliases? &
+ NodesOfContainerCluster?
+}
+
+ContainerServices =
+ RestApi* &
+ Servlet* &
+ SearchInContainer? &
+ DocprocInContainer? &
+ ProcessingInContainer? &
+ DocumentApi? &
+ Components* &
+ Component* &
+ Handler* &
+ Client* &
+ Server* &
+ Http? &
+ HttpFilter? &
+ AccessLog* &
+ GenericConfig*
+
+Components = element components {
+ Include* &
+ Component*
+}
+
+Aliases = element aliases {
+ element service-alias { xsd:NCName }* &
+ element endpoint-alias { xsd:NCName }*
+}
+
+Include = element \include {
+ attribute dir { text }
+}
+
+Http = element http {
+ (Filtering & HttpServer+) |
+ HttpServer+ |
+ empty
+}
+
+Filtering = element filtering {
+ HttpFilter* &
+ element request-chain {
+ HttpFilterChain
+ }* &
+ element response-chain {
+ HttpFilterChain
+ }*
+}
+
+HttpServer = element server {
+ attribute port { xsd:nonNegativeInteger } &
+ ComponentId &
+ GenericConfig*
+}
+
+AccessLog = element accesslog {
+ attribute type { "yapache" | "vespa" | "disabled" }? &
+ attribute fileNamePattern { string }? &
+ attribute symlinkName { string }? &
+
+ attribute rotationInterval { string }? &
+ attribute rotationScheme { "date" | "sequence" }?
+}
+
+# REST-API:
+
+RestApi = element rest-api {
+ attribute path { xsd:string { pattern = "\w[\w_/\-\.]*" } } &
+ attribute jersey2 { xsd:boolean }? &
+ element components {
+ attribute bundle { xsd:Name } &
+ element package { xsd:NCName }*
+ }*
+
+# element inject {
+# attribute component { xsd:Name } &
+# attribute for-name { xsd:Name }
+# }*
+}
+
+# Servlet:
+
+Servlet = element servlet {
+ ComponentDefinition &
+ ServletConfig? &
+ element path { xsd:string { pattern = "\w[\w_/\-\.]*(\*)?" } }
+}
+
+ServletConfig = element servlet-config {
+ anyElement+
+}
+
+# SEARCH:
+
+SearchInContainer = element search {
+ Include* &
+ ServerBindings? &
+ Searcher* &
+ SearchChain* &
+ Provider* &
+ Renderer* &
+ GenericConfig*
+}
+
+SearchChain = element chain {
+ GenericSearchChain
+}
+
+GenericSearchChain =
+ ComponentId &
+ SearchChainInheritance &
+ attribute searchers { text }? &
+ Searcher* &
+ Phase* &
+ GenericConfig*
+
+SearchChainInheritance =
+ attribute inherits { text }? &
+ attribute excludes { text }? &
+ element inherits {
+ element chain { ComponentSpec }* &
+ element exclude { ComponentSpec }*
+ }?
+
+
+# DOCPROC:
+
+DocprocInContainer = element document-processing {
+ Include* &
+ DocprocClusterAttributes? &
+ DocumentProcessorV3* &
+ ChainInDocprocInContainerCluster* &
+ GenericConfig*
+}
+ChainInDocprocInContainerCluster = element chain {
+ DocprocChainV3Contents
+}
+
+
+
+# PROCESSING:
+
+ProcessingInContainer = element processing {
+ Include* &
+ ServerBindings? &
+ Processor* &
+ Chain* &
+ ProcessingRenderer* &
+ GenericConfig*
+}
+
+
+
+# DOCUMENT API/GATEWAY:
+
+DocumentApi = element document-api {
+ ServerBindings &
+ GenericConfig* &
+ element abortondocumenterror { xsd:boolean }? &
+ element retryenabled { xsd:boolean }? &
+ element route { text }? &
+ element maxpendingdocs { xsd:positiveInteger }? &
+ element maxpendingbytes { xsd:positiveInteger }? &
+ element retrydelay { xsd:double { minInclusive = "0.0" } }? &
+ element timeout { xsd:double { minInclusive = "0.0" } }? &
+ element tracelevel { xsd:positiveInteger }? &
+ element mbusport { xsd:positiveInteger }?
+}
+
+
+
+# NODES:
+
+NodesOfContainerCluster = element nodes {
+ attribute jvmargs { text }? &
+ attribute preload { text }? &
+ attribute cpu-socket-affinity { xsd:boolean }? &
+ (
+ (
+ attribute count { xsd:positiveInteger } &
+ attribute flavor { xsd:string }? &
+ attribute docker-image { xsd:string }?
+ )
+ |
+ element node {
+ attribute hostalias { xsd:NCName } &
+ attribute cpu-socket { xsd:positiveInteger }? &
+ ServerPort* &
+ GenericConfig*
+ }+
+ )
+}
+
+ServerPort = element server-port {
+ ComponentId &
+ attribute port { xsd:positiveInteger }
+}
+
+
+#DOCUMENT BINDINGS:
+
+DocumentBinding = element document {
+ attribute type { xsd:NCName } &
+ attribute class { xsd:NCName } &
+ attribute bundle { xsd:NCName }
+}
+
diff --git a/config-model/src/main/resources/schema/content.rnc b/config-model/src/main/resources/schema/content.rnc
new file mode 100644
index 00000000000..36a8dd276ef
--- /dev/null
+++ b/config-model/src/main/resources/schema/content.rnc
@@ -0,0 +1,254 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
+
+Redundancy = element redundancy {
+ attribute reply-after { xsd:nonNegativeInteger }? &
+ xsd:nonNegativeInteger
+}
+
+DistributionType = element distribution {
+ attribute type { "strict" | "loose" | "legacy" }
+}
+
+BucketSplitting = element bucket-splitting {
+ attribute max-documents { xsd:nonNegativeInteger }? &
+ attribute max-size { xsd:nonNegativeInteger }? &
+ attribute minimum-bits { xsd:nonNegativeInteger }?
+}
+
+MergeTuning = element merges {
+ attribute max-per-node { xsd:nonNegativeInteger }? &
+ attribute max-queue-size { xsd:nonNegativeInteger }? &
+ attribute max-nodes-per-merge { xsd:nonNegativeInteger {
+ minInclusive = "2" maxInclusive = "16" } }?
+}
+
+VisitorMaxConcurrent = element max-concurrent {
+ attribute fixed { xsd:nonNegativeInteger }? &
+ attribute variable { xsd:nonNegativeInteger }?
+}
+
+VisitorTuning = element visitors {
+ attribute thread-count { xsd:nonNegativeInteger }? &
+ attribute max-queue-size { xsd:nonNegativeInteger }? &
+ VisitorMaxConcurrent?
+}
+
+Maintenance = element maintenance {
+ attribute start { xsd:string { pattern = "[0-9]{2}:[0-9]{2}" } },
+ attribute stop { xsd:string { pattern = "[0-9]{2}:[0-9]{2}" } },
+ attribute high { "monday" | "tuesday" | "wednesday" | "thursday" |
+ "friday" | "saturday" | "sunday" }
+}
+
+PersistenceThread = element thread {
+ ## The lowest priority this thread should handle.
+ attribute lowest-priority { "HIGHEST" | "VERY_HIGH" | "HIGH_1" |
+ "HIGH_2" | "HIGH_3" | "NORMAL_1" | "NORMAL_2" | "NORMAL_3" |
+ "NORMAL_4" | "NORMAL_5" | "NORMAL_6" | "LOW_1" |
+ "LOW_2" | "LOW_3" | "VERY_LOW" }? &
+ ## The number of threads of this type to create
+ attribute count { xsd:integer }?
+}
+
+## Declare which storage threads each disk should have.
+PersistenceThreads = element persistence-threads {
+ ## Operations with priority worse than this can be blocked
+ attribute highest-priority-to-block { xsd:string } ? &
+ ## Operations with priority better than this can block others
+ attribute lowest-priority-to-block-others { xsd:string } ? &
+ Thread+
+}
+
+ClusterControllerTuning = element cluster-controller {
+ element init-progress-time { xsd:string { pattern = "([0-9\.]+)\s*([a-z]+)?" } }? &
+ element transition-time { xsd:string { pattern = "([0-9\.]+)\s*([a-z]+)?" } }? &
+ element max-premature-crashes { xsd:nonNegativeInteger }? &
+ element stable-state-period { xsd:string { pattern = "([0-9\.]+)\s*([a-z]+)?" } }? &
+ element min-distributor-up-ratio { xsd:double }? &
+ element min-storage-up-ratio { xsd:double }?
+}
+
+DispatchTuning = element dispatch {
+ element max-hits-per-partition { xsd:nonNegativeInteger }? &
+ element dispatch-policy { "round-robin" | "random" }? &
+ element min-group-coverage { xsd:double }? &
+ element min-active-docs-coverage { xsd:double }? &
+ element use-local-node { "true" | "false" }?
+}
+
+ClusterTuning = element tuning {
+ DispatchTuning? &
+ DistributionType? &
+ BucketSplitting? &
+ MergeTuning? &
+ VisitorTuning? &
+ ClusterControllerTuning? &
+ Maintenance? &
+ PersistenceThreads?
+}
+
+Content = element content {
+ attribute version { "1.0" } &
+ attribute id { xsd:NCName }? &
+ attribute distributor-base-port { xsd:unsignedShort }? &
+ # Mandatory
+ Redundancy &
+ ContentSearch? &
+ Dispatch? &
+ ClusterTuning? &
+ # Can be used for caches and feedbatching.
+ GenericConfig* &
+ Engine? &
+ # Here you can add document definitions that you also want to handle.
+ # Search might want to know of them in advance.
+ Documents? &
+ (ContentNodes | TopGroup) &
+ Controllers?
+}
+
+Controllers =
+ element controllers {
+ OptionalDedicatedNodes
+ }
+
+ContentSearch = element search {
+ element query-timeout { xsd:double { minInclusive = "0" } }? &
+ element visibility-delay { xsd:double { minInclusive = "0" } }? &
+ SearchCoverage?
+}
+
+SearchCoverage = element coverage {
+ element minimum { xsd:double { minInclusive = "0" maxInclusive = "1" } }? &
+ element min-wait-after-coverage-factor { xsd:double { minInclusive = "0" maxInclusive = "1" } }? &
+ element max-wait-after-coverage-factor { xsd:double { minInclusive = "0" maxInclusive = "1" } }?
+}
+
+Dispatch = element dispatch {
+ element num-dispatch-groups { xsd:nonNegativeInteger }? &
+ DispatchGroup*
+}
+
+DispatchGroup = element group {
+ DispatchNode+
+}
+
+DispatchNode = element node {
+ attribute distribution-key { xsd:nonNegativeInteger }
+}
+
+## Specification of what document processing should be done for indexing.
+DocumentProcessing = element document-processing {
+ attribute cluster { text }? &
+ attribute chain { text }?
+}
+
+## Config for the persistence providers.
+Engine = element engine {
+ (Vds | Proton | Dummy)
+}
+
+Vds = element vds {
+ text
+}
+
+Proton = element proton {
+ element flush-on-shutdown { xsd:string }? &
+ element visibility-delay { xsd:double { minInclusive = "0.0" } }? &
+ element query-timeout { xsd:double { minInclusive = "0.0" } }? &
+ element searchable-copies { xsd:integer { minInclusive = "0" } }? &
+ ResourceLimits? &
+ Tuning?
+}
+
+ResourceLimits = element resource-limits {
+ element disk { xsd:double { minInclusive = "0.0" maxInclusive = "1.0" } }? &
+ element memory { xsd:double { minInclusive = "0.0" maxInclusive = "1.0" } }?
+}
+
+Dummy = element dummy {
+ text
+}
+
+Documents = element documents {
+ attribute selection { xsd:string }? &
+ attribute garbage-collection { xsd:string }? &
+ attribute garbage-collection-interval { xsd:nonNegativeInteger }? &
+ DocumentProcessing? &
+
+ element document {
+ GenericConfig* &
+ attribute type { xsd:string } &
+ attribute selection { xsd:string }? &
+ attribute mode { "index" | "streaming" | "store-only" }
+ }+
+}
+
+ContentNode = element node {
+ GenericConfig* &
+ service.attlist &
+ attribute distribution-key { xsd:nonNegativeInteger } &
+ attribute capacity { xsd:double { minExclusive = "0.0" } }? &
+ attribute cpu-socket { xsd:nonNegativeInteger }? &
+ attribute mmap-nocore-limit { xsd:nonNegativeInteger }?
+}
+
+ContentNodes = element nodes {
+ attribute cpu-socket-affinity { xsd:string }? &
+ attribute mmap-nocore-limit { xsd:nonNegativeInteger }? &
+ (
+ (
+ attribute count { xsd:positiveInteger } &
+ attribute flavor { xsd:string }? &
+ attribute docker-image { xsd:string }? &
+ attribute groups { xsd:positiveInteger }?
+ )
+ |
+ ContentNode +
+ )
+}
+
+TopGroup = element group {
+ # Neither name nor distribution key makes any sense for the top group. There has to be
+ # a top group so it never needs referring to, and it's only one group to choose from so
+ # it has no use of a distribution key. Leaving it allowed to set them for now to not
+ # break all system tests and backward compatibility.
+ attribute name { xsd:string }? &
+ attribute cpu-socket-affinity { xsd:string }? &
+ attribute mmap-nocore-limit { xsd:nonNegativeInteger }? &
+ attribute distribution-key { xsd:nonNegativeInteger }? &
+ (
+ ContentNode +
+ |
+ (
+ element distribution {
+ attribute partitions { xsd:string }
+ } &
+ Group +
+ )
+ )
+}
+
+Group = element group {
+ attribute distribution-key { xsd:nonNegativeInteger } &
+ attribute name { xsd:string } &
+ (
+ ContentNode +
+ |
+ (
+ element nodes {
+ attribute count { xsd:positiveInteger } &
+ attribute flavor { xsd:string }? &
+ attribute docker-image { xsd:string }? &
+ attribute groups { xsd:positiveInteger }?
+ }
+ )
+ |
+ (
+ element distribution {
+ attribute partitions { xsd:string }
+ } &
+ Group +
+ )
+ )
+}
diff --git a/config-model/src/main/resources/schema/docproc-standalone.rnc b/config-model/src/main/resources/schema/docproc-standalone.rnc
new file mode 100644
index 00000000000..55cea9c2fc2
--- /dev/null
+++ b/config-model/src/main/resources/schema/docproc-standalone.rnc
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+include "common.rnc"
+include "container.rnc"
+include "searchchains.rnc"
+include "docproc.rnc"
+start = DocprocChainsV3
diff --git a/config-model/src/main/resources/schema/docproc.rnc b/config-model/src/main/resources/schema/docproc.rnc
new file mode 100644
index 00000000000..9741182d311
--- /dev/null
+++ b/config-model/src/main/resources/schema/docproc.rnc
@@ -0,0 +1,98 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+DocProc = element docproc {
+ DocProcV3
+}
+
+SchemaMapping = element map {
+ element field {
+ attribute doctype { text }?,
+ attribute in-document { text },
+ attribute in-processor { text }
+ }+
+}
+
+
+
+
+#Version 3 config:
+
+DocProcV3 = attribute version { "3.0" },
+ (ClusterV3* &
+ OuterDocprocChainsV3? &
+ GenericConfig*
+)
+
+DocprocClusterAttributes = attribute compressdocuments { xsd:boolean }? &
+ attribute numnodesperclient { xsd:positiveInteger }? &
+ attribute preferlocalnode { xsd:boolean }? &
+ attribute maxmessagesinqueue { xsd:nonNegativeInteger }? &
+ attribute maxqueuebytesize { xsd:string { minLength = "1" } }? &
+ attribute maxqueuewait { xsd:positiveInteger }? &
+ attribute maxconcurrentfactor { xsd:double { minExclusive = "0.0" maxExclusive = "1.0" } }? &
+ attribute documentexpansionfactor { xsd:double { minExclusive = "0.0" } }? &
+ attribute containercorememory { xsd:nonNegativeInteger }?
+
+ClusterV3 = element cluster {
+ attribute name { xsd:NCName } &
+ DocprocClusterAttributes? &
+
+ element nodes {
+ attribute jvmargs { text }? &
+ attribute preload { text }? &
+ element node {
+ GenericConfig* &
+ service.attlist &
+ attribute maxmessagesinqueue { xsd:nonNegativeInteger }? &
+ attribute maxqueuebytesize { xsd:string { minLength = "1" } }? &
+ attribute maxqueuewait { xsd:positiveInteger }?
+ }+
+ } &
+ GenericConfig* &
+ SchemaMapping? &
+ Component* &
+ Handler* &
+ DocprocChainsV3?
+}
+
+DocprocChainsV3 =
+ element docprocchains {
+ DocumentProcessorV3* &
+ DocprocChainV3* &
+ GenericConfig*
+ }
+
+OuterDocprocChainsV3 =
+ element docprocchains {
+ DocumentProcessorV3* &
+ DocprocChainV3*
+ }
+
+DocprocChainV3 =
+ element docprocchain {
+ DocprocChainV3Contents
+ }
+
+DocprocChainV3Contents = attribute name { xsd:NCName }? &
+ ComponentId &
+ SchemaMapping? &
+ DocprocChainInheritance &
+ attribute documentprocessors { text }? &
+ DocumentProcessorV3* &
+ Phase* &
+ GenericConfig*
+
+
+DocprocChainInheritance =
+ attribute inherits { text }? &
+ attribute excludes { text }? &
+ element inherits {
+ element docprocchain { ComponentSpec }* &
+ element exclude { ComponentSpec }*
+ }?
+
+DocumentProcessorV3 =
+ element documentprocessor {
+ BundleSpec &
+ SchemaMapping? &
+ GenericSearcherOrDocumentProcessor
+ }
diff --git a/config-model/src/main/resources/schema/federation.rnc b/config-model/src/main/resources/schema/federation.rnc
new file mode 100644
index 00000000000..81933cc0622
--- /dev/null
+++ b/config-model/src/main/resources/schema/federation.rnc
@@ -0,0 +1,73 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Schema for federation configuration inside the searchchains section.
+
+GenericSource =
+ GenericSearchChainInQrservers &
+ FederationOptions?
+
+Source =
+ element source {
+ GenericSource
+ }
+
+Provider =
+ element provider {
+ GenericSource &
+ attribute cachesize { xsd:string { pattern = "\d+(\.\d*)?\s*[kmgKMG]?" } }? &
+ attribute type { xsd:string }? &
+ attribute cluster { xsd:string }? &
+ attribute yca-application-id { xsd:string }? &
+ attribute yca-cache-ttl { xsd:string { pattern = "\d+(\.\d*)?\s*m?s" } }? &
+ attribute yca-cache-retry-wait { xsd:string { pattern = "\d+(\.\d*)?\s*m?s" } }? &
+ YcaProxy? &
+ HttpProviderSearcherOptions &
+
+ Source*
+ }
+
+YcaProxy =
+ element yca-proxy {
+ GenericNode?
+ }
+
+GenericNode =
+ attribute host { xsd:string } &
+ attribute port { xsd:int }
+
+
+HttpProviderSearcherOptions =
+ attribute cacheweight { xsd:float { minInclusive = "0" } }? &
+ attribute path { xsd:string }? &
+ attribute readtimeout { xsd:string { pattern = "\d+(\.\d*)?\s*m?s" } }? &
+ attribute connectiontimeout { xsd:string { pattern = "\d+(\.\d*)?\s*m?s" } }? &
+ attribute connectionpooltimeout { xsd:string { pattern = "\d+(\.\d*)?\s*m?s" } }? &
+ attribute retries { xsd:int }? &
+ element nodes {
+ element node {
+ attribute host { xsd:string } &
+ attribute port { xsd:int }
+ }+
+ } ?
+
+FederationOptions =
+ element federationoptions {
+ attribute optional { xsd:boolean }? &
+ attribute timeout { xsd:string { pattern = "\d+(\.\d*)?\s*m?s" } }? &
+ attribute requestTimeout { xsd:string { pattern = "\d+(\.\d*)?\s*m?s" } }? &
+ attribute default { xsd:boolean }?
+ }
+
+FederationSearcher =
+ element federation {
+ GenericSearcherOrDocumentProcessor &
+ element source {
+ ComponentSpec &
+ FederationOptions?
+ }* &
+ element target-selector {
+ ComponentDefinition
+ }? &
+ element source-set {
+ attribute inherits {"default"}
+ }?
+ }
diff --git a/config-model/src/main/resources/schema/genericcluster.rnc b/config-model/src/main/resources/schema/genericcluster.rnc
new file mode 100644
index 00000000000..53a35ecce44
--- /dev/null
+++ b/config-model/src/main/resources/schema/genericcluster.rnc
@@ -0,0 +1,22 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Generic, application-specific service cluster
+#
+include "genericmodule.rnc"
+
+GenericCluster = element service {
+ attribute version { "1.0" } &
+ attribute id { xsd:NCName }? &
+ attribute name { text } &
+ attribute command { text } &
+ attribute hostservice { text }? &
+ attribute num-hosts { text }? &
+ GenericConfig* &
+ GenericModule* &
+ element node {
+ service.attlist &
+ attribute name { text }? &
+ GenericModule* &
+ GenericConfig*
+ }*
+}
+
diff --git a/config-model/src/main/resources/schema/genericmodule.rnc b/config-model/src/main/resources/schema/genericmodule.rnc
new file mode 100644
index 00000000000..f0933debccb
--- /dev/null
+++ b/config-model/src/main/resources/schema/genericmodule.rnc
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Generic, nestable module
+
+GenericModule = element module {
+ attribute name { text } &
+ GenericConfig* &
+ GenericModule*
+}
diff --git a/config-model/src/main/resources/schema/hosts.rnc b/config-model/src/main/resources/schema/hosts.rnc
new file mode 100644
index 00000000000..a55f9caf7e1
--- /dev/null
+++ b/config-model/src/main/resources/schema/hosts.rnc
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# RELAX NG Compact Syntax
+# Vespa Hosts file
+
+element hosts {
+ element host {
+ attribute name { text },
+ element alias { text }*
+ }+
+}
diff --git a/config-model/src/main/resources/schema/legacygenericcluster.rnc b/config-model/src/main/resources/schema/legacygenericcluster.rnc
new file mode 100644
index 00000000000..a6f51a6c468
--- /dev/null
+++ b/config-model/src/main/resources/schema/legacygenericcluster.rnc
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Generic, application-specific service cluster
+#
+include "legacygenericmodule.rnc"
+
+LegacyGenericCluster = element cluster {
+ attribute name { text } &
+ attribute command { text } &
+ attribute hostservice { text }? &
+ attribute num-hosts { text }? &
+ GenericConfig* &
+ LegacyGenericModule* &
+ element node {
+ service.attlist &
+ attribute name { text }? &
+ LegacyGenericModule* &
+ GenericConfig*
+ }*
+}
+
diff --git a/config-model/src/main/resources/schema/legacygenericmodule.rnc b/config-model/src/main/resources/schema/legacygenericmodule.rnc
new file mode 100644
index 00000000000..f896a3e776b
--- /dev/null
+++ b/config-model/src/main/resources/schema/legacygenericmodule.rnc
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Generic, nestable module
+
+LegacyGenericModule = element module {
+ attribute name { text } &
+ GenericConfig* &
+ LegacyGenericModule*
+}
diff --git a/config-model/src/main/resources/schema/processing.rnc b/config-model/src/main/resources/schema/processing.rnc
new file mode 100644
index 00000000000..b043034b502
--- /dev/null
+++ b/config-model/src/main/resources/schema/processing.rnc
@@ -0,0 +1,39 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Schema for processing components and chains
+
+ProcessingRenderer = element renderer {
+ ComponentDefinition
+}
+
+Processing =
+ element processing {
+ Processor* &
+ Chain* &
+ ProcessingRenderer* &
+ GenericConfig*
+ }
+
+ChainBaseContent =
+ ComponentId &
+ ChainInheritance &
+ GenericConfig*
+
+Chain = element chain {
+ ChainBaseContent &
+ Processor* &
+ Phase*
+ }
+
+ChainInheritance =
+ attribute inherits { text }? &
+ attribute excludes { text }? &
+ element inherits {
+ element chain { ComponentSpec }* &
+ element exclude { ComponentSpec }*
+ }?
+
+Processor =
+ element processor {
+ BundleSpec &
+ GenericSearcherOrDocumentProcessor
+ }
diff --git a/config-model/src/main/resources/schema/routing-standalone.rnc b/config-model/src/main/resources/schema/routing-standalone.rnc
new file mode 100644
index 00000000000..f84c38ab5ca
--- /dev/null
+++ b/config-model/src/main/resources/schema/routing-standalone.rnc
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+include "common.rnc"
+include "routing.rnc"
+start = Routing
diff --git a/config-model/src/main/resources/schema/routing.rnc b/config-model/src/main/resources/schema/routing.rnc
new file mode 100644
index 00000000000..2ba8f11bbf3
--- /dev/null
+++ b/config-model/src/main/resources/schema/routing.rnc
@@ -0,0 +1,28 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# RELAX NG compact syntax pattern
+# for Vespa MessageBus explicit routing config
+Routing = element routing {
+ attribute version { "1.0" } &
+ element routingtable {
+ attribute protocol { "document" } &
+ attribute verify { xsd:boolean }? &
+ element hop {
+ attribute name { text } &
+ attribute selector { text } &
+ attribute ignore-result { xsd:boolean }? &
+ element recipient {
+ attribute session { text }
+ }*
+ }* &
+ element route {
+ attribute name { text } &
+ attribute hops { text }
+ }*
+ }* &
+ element services {
+ attribute protocol { "document" } &
+ element service {
+ attribute name { text }
+ }*
+ }*
+}
diff --git a/config-model/src/main/resources/schema/schemas.xml b/config-model/src/main/resources/schema/schemas.xml
new file mode 100644
index 00000000000..728754e3a5f
--- /dev/null
+++ b/config-model/src/main/resources/schema/schemas.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<locatingRules xmlns="http://thaiopensource.com/ns/locating-rules/1.0">
+ <documentElement localName="hosts" uri="hosts.rnc"/>
+ <documentElement localName="services" uri="services.rnc"/>
+</locatingRules>
diff --git a/config-model/src/main/resources/schema/search.rnc b/config-model/src/main/resources/schema/search.rnc
new file mode 100644
index 00000000000..19a3860d93b
--- /dev/null
+++ b/config-model/src/main/resources/schema/search.rnc
@@ -0,0 +1,234 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
+
+include "searchchains.rnc"
+include "container.rnc"
+
+Search = element search {
+ attribute version { "2.0" } &
+ GenericConfig* &
+ (QrserversWithClusters | QRServers)? &
+ SearchDefinitions? &
+ SCluster*
+}
+
+ModeStreaming = attribute indexingmode { string "streaming" | string "STREAMING" } &
+ attribute storagecluster { xsd:NCName }
+
+QRServerOptionsElems =
+ element cacheoptions {
+ attribute cluster { list { xsd:NCName+ } }? &
+ ## Size in megabytes
+ element size { xsd:nonNegativeInteger }?
+ }* &
+ element resultlimits {
+ attribute maxhits { xsd:nonNegativeInteger }? &
+ attribute maxoffset { xsd:nonNegativeInteger }? &
+ attribute maxgroups { xsd:nonNegativeInteger }?
+ }? &
+ element accesslog { string "queryaccesslog" | string "yapacheaccesslog" | string "disabled" }?
+
+SearchDefinitions = element searchdefinitions {
+ element searchdefinition {
+ attribute name { text }
+ }+
+}
+
+TopLevelDispatchers = element topleveldispatchers {
+ element topleveldispatcher {
+ attribute preferlocalrow { text }? &
+ service.attlist &
+ GenericConfig*
+ }+ &
+ GenericConfig*
+}
+
+QRServers = element qrservers {
+ GenericConfig* &
+ Container &
+ element qrserveroptions {
+ QRServerOptionsElems
+ }? &
+ attribute jvmargs { text }? &
+ attribute preload { text }? &
+ element qrserver {
+ service.attlist &
+ GenericConfig*
+ }+ &
+ element cache {
+ attribute size { xsd:string { pattern = "\d+(\.\d*)?\s*[kmgKMG]?" } }
+ }?
+}
+
+QrserversWithClusters = element qrservers {
+ GenericConfig* &
+ OuterSearchChains? &
+ element cluster {
+ attribute name { xsd:NCName } &
+ GenericConfig* &
+ Container &
+ element qrserveroptions {
+ QRServerOptionsElems
+ }? &
+ element nodes {
+ attribute jvmargs { text }? &
+ attribute preload { text }? &
+ element node {
+ service.attlist &
+ GenericConfig*
+ }+
+ }+ &
+ element cache {
+ attribute size { xsd:string { pattern = "\d+(\.\d*)?\s*[kmgKMG]?" } }
+ }?
+ }+
+}
+
+Container =
+ Handler* &
+ Client* &
+ Server* &
+ Component* &
+ HttpFilter? &
+ Renderer* &
+ SearchChains? &
+ Processing?
+
+ModeRealTime = attribute indexingmode { string "realtime" | string "REALTIME" }
+
+ClusterCommon = attribute name { xsd:NCName }? &
+ GenericConfig* &
+ ( ModeRealTime | ModeStreaming )? &
+ # TODO: Rename?
+ element visibilitydelay { xsd:double { minInclusive = "0.0" } }? &
+ element querytimeout { xsd:double { minInclusive = "0.0" } }? &
+ SearchDefinitions
+
+
+Indexing = element indexing {
+ attribute indexingclustername { text }? &
+ attribute chain { text }? &
+ #Below element is deprecated and has no effect:
+ element documentprocessors {
+ DocumentProcessorV3+
+ }?
+}
+
+SCluster = element cluster {
+ # name defaults to 'search'
+ ClusterCommon &
+ attribute minnodespercolumn { xsd:nonNegativeInteger }? &
+ Indexing? &
+ element documents {
+ attribute feedname { text }?,
+ # TODO: check if 'true' works for getting all, and disallow empty in selection language.
+ attribute selection { text }?
+ }? &
+ # TODO: remove in the future...
+ element resultlimits {
+ attribute maxhits { xsd:nonNegativeInteger }? &
+ attribute maxoffset { xsd:nonNegativeInteger }?
+ }? &
+ element oos {
+ attribute auto { xsd:boolean } &
+ attribute timeout { xsd:nonNegativeInteger }? &
+ attribute min-in-service-per-column { xsd:positiveInteger }?
+ }? &
+ # Default is that one clustercontroller is created on a qrserver node in javacode
+ element clustercontrollers {
+ element clustercontroller { service.attlist & GenericConfig* }+
+ }? &
+ # Defaults is that two topleveldispatchers are created (one each on different
+ # qrserver nodes, only one created if one qrserver node specified) in javacode
+ TopLevelDispatchers? &
+ element row {
+ GenericConfig* &
+ (attribute index { xsd:nonNegativeInteger },
+ element searchnodes {
+ element searchnode {
+ service.attlist &
+ GenericConfig* &
+ attribute index { xsd:nonNegativeInteger } &
+ attribute engine { text }?
+ }+
+ })
+ }* &
+ Tuning?
+}
+
+Tuning = element tuning {
+ element dispatch {
+ element max-hits-per-partition { xsd:nonNegativeInteger }?
+ }? &
+ element searchnode {
+ element requestthreads {
+ element search { xsd:nonNegativeInteger }? &
+ element persearch { xsd:nonNegativeInteger }? &
+ element summary { xsd:nonNegativeInteger }?
+ }? &
+ element flushstrategy {
+ element native {
+ element total {
+ element maxmemorygain { xsd:nonNegativeInteger }? &
+ element diskbloatfactor { xsd:double { minInclusive = "0.0" } }?
+ }? &
+ element component {
+ element maxmemorygain { xsd:nonNegativeInteger }? &
+ element diskbloatfactor { xsd:double { minInclusive = "0.0" } }? &
+ element maxage { xsd:nonNegativeInteger }?
+ }? &
+ element transactionlog {
+ element maxentries { xsd:nonNegativeInteger }? &
+ element maxsize { xsd:nonNegativeInteger }?
+ }?
+ }?
+ }? &
+ element resizing {
+ element initialdocumentcount { xsd:nonNegativeInteger }?
+ }? &
+ element index {
+ element io {
+ element write { TuningIoOptionsLight }? &
+ element read { TuningIoOptionsLight }? &
+ element search { TuningIoOptionsFull }?
+ }
+ }? &
+ element attribute {
+ element io {
+ element write { TuningIoOptionsLight }?
+ }
+ }? &
+ element summary {
+ element io {
+ element write { TuningIoOptionsLight }? &
+ element read { TuningIoOptionsFull }?
+ }? &
+ element store {
+ element cache {
+ element maxsize { xsd:nonNegativeInteger }? &
+ element initialentries { xsd:nonNegativeInteger }? &
+ element maxentries { xsd:nonNegativeInteger }? &
+ TuningCompression?
+ }? &
+ element logstore {
+ element maxfilesize { xsd:nonNegativeInteger }? &
+ element maxdiskbloatfactor { xsd:double { minInclusive = "0.0" } }? &
+ element minfilesizefactor { xsd:double { minInclusive = "0.10" maxInclusive = "1.0" } }? &
+ element chunk {
+ element maxsize { xsd:nonNegativeInteger }? &
+ element maxentries { xsd:nonNegativeInteger }? &
+ TuningCompression?
+ }?
+ }?
+ }?
+ }?
+ }?
+}
+
+TuningIoOptionsLight = string "normal" | string "directio"
+TuningIoOptionsFull = string "normal" | string "directio" | string "mmap" | string "mlock"
+
+TuningCompression = element compression {
+ element type { string "none" | string "lz4" }? &
+ element level { xsd:nonNegativeInteger }?
+}
diff --git a/config-model/src/main/resources/schema/searchchains-standalone.rnc b/config-model/src/main/resources/schema/searchchains-standalone.rnc
new file mode 100644
index 00000000000..ef6a3b47eb7
--- /dev/null
+++ b/config-model/src/main/resources/schema/searchchains-standalone.rnc
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+include "common.rnc"
+include "searchchains.rnc"
+start = SearchChains
diff --git a/config-model/src/main/resources/schema/searchchains.rnc b/config-model/src/main/resources/schema/searchchains.rnc
new file mode 100644
index 00000000000..8b11b900557
--- /dev/null
+++ b/config-model/src/main/resources/schema/searchchains.rnc
@@ -0,0 +1,74 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#Schema for search chains and searchers inside the searchchains section.
+
+include "federation.rnc"
+
+SearchChains =
+ element searchchains {
+ Searcher* &
+ SearchChainInQrservers* &
+ GenericConfig*
+ }
+
+# TODO: Cannot have generic config (see bug #4766780)
+OuterSearchChains =
+ element searchchains {
+ Searcher* &
+ SearchChainInQrservers* &
+ GenericConfig* # TODO: Remove when we no longer need the QRServersV2 syntax (qrs without clusters)
+ }
+
+SearchChainInQrservers =
+ element searchchain {
+ GenericSearchChainInQrservers
+ } |
+ Provider
+
+GenericSearchChainInQrservers =
+ ComponentId &
+ SearchChainInheritanceInQrservers &
+ attribute searchers { text }? &
+ Searcher* &
+ Phase* &
+ GenericConfig*
+
+SearchChainInheritanceInQrservers =
+ attribute inherits { text }? &
+ attribute excludes { text }? &
+ element inherits {
+ element searchchain { ComponentSpec }* &
+ element exclude { ComponentSpec }*
+ }?
+
+Searcher =
+ RegularSearcher |
+ FederationSearcher
+
+RegularSearcher =
+ element searcher {
+ BundleSpec &
+ GenericSearcherOrDocumentProcessor
+ }
+
+
+GenericSearcherOrDocumentProcessor =
+ ComponentId &
+ SearcherOrDocumentProcessorDependencies &
+ GenericConfig*
+
+SearcherOrDocumentProcessorDependencies =
+ Dependencies &
+ attribute provides { text }? &
+ element provides { xsd:Name }*
+
+Dependencies =
+ attribute before { text }? &
+ attribute after { text }? &
+ element before { xsd:Name }* &
+ element after { xsd:Name }*
+
+Phase =
+ element phase {
+ ComponentId &
+ Dependencies
+ }
diff --git a/config-model/src/main/resources/schema/services.rnc b/config-model/src/main/resources/schema/services.rnc
new file mode 100644
index 00000000000..c52ed90bbd4
--- /dev/null
+++ b/config-model/src/main/resources/schema/services.rnc
@@ -0,0 +1,31 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+include "common.rnc"
+include "admin.rnc"
+include "clients.rnc"
+include "content.rnc"
+include "docproc.rnc"
+include "storage.rnc"
+include "search.rnc"
+include "routing.rnc"
+include "containercluster.rnc"
+include "genericcluster.rnc"
+include "legacygenericcluster.rnc"
+
+start = element services {
+ attribute version { "1.0" }? &
+ attribute major-version { text }? &
+ LegacyGenericCluster* &
+ GenericCluster* &
+ GenericConfig* &
+ Admin? &
+ Clients? &
+ Content* &
+ ContainerCluster* &
+ Routing?
+}
+| GenericCluster
+| Admin
+| Clients
+| Content
+| ContainerCluster
+
diff --git a/config-model/src/main/resources/schema/storage.rnc b/config-model/src/main/resources/schema/storage.rnc
new file mode 100644
index 00000000000..e31e503fe69
--- /dev/null
+++ b/config-model/src/main/resources/schema/storage.rnc
@@ -0,0 +1,237 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
+
+Storage = Storage30
+
+# Specific v2.0 definitions
+
+Tuning20 = element tuning {
+ element cache { xsd:nonNegativeInteger }? &
+ element memorytouse { xsd:nonNegativeInteger }? &
+ element avgdiskspernode { xsd:nonNegativeInteger }? &
+ element avgmegabytesperdisk { xsd:nonNegativeInteger }? &
+ element avgdocsize { xsd:nonNegativeInteger }?
+}
+
+Datadistribution = element datadistribution {
+ attribute disk { text }? &
+ element diskbalancer {
+ attribute start { xsd:string { pattern = "[0-9]{2}:[0-9]{2}" } } ? &
+ attribute stop { xsd:string { pattern = "[0-9]{2}:[0-9]{2}" } } ? &
+ attribute cycletime { xsd:string { pattern = "[0-9]{2}:[0-9]{2}" } } ? &
+ attribute targetskewpercent { xsd:double { minInclusive = "0" maxInclusive = "100"} }? &
+ attribute max-target-fill-rate-above-average { xsd:double { minInclusive = "-1" maxInclusive = "1"} }? &
+ attribute stopontarget { xsd:boolean }?
+ } ?
+}
+
+OperationsLog20 =
+## File name of file to log operations to.
+[ a:defaultValue = "false" ]
+element operationslog { xsd:boolean }
+
+Priority = element priority {
+ attribute from { xsd:string} &
+ attribute to { xsd:integer { minInclusive = "0" maxInclusive = "255" } }
+}
+
+PriorityMapping = element prioritymapping {
+ Priority+
+}
+
+Thread = element thread {
+ ## The lowest priority this thread should handle.
+ attribute lowest-priority { xsd:string}? &
+ ## The number of threads of this type to create
+ attribute count { xsd:integer }?
+}
+
+## Declare which storage threads each disk should have.
+Threads = element diskthreads {
+ ## Operations with priority worse than this can be blocked
+ attribute highest-priority-to-block { xsd:string } ? &
+ ## Operations with priority better than this can block others
+ attribute lowest-priority-to-block-others { xsd:string } ? &
+ Thread+
+}
+
+Visitors = element visitors {
+ attribute threads { xsd:positiveInteger }? &
+ attribute maxqueuesize { xsd:positiveInteger }? &
+ element maxconcurrent {
+ attribute fixed { xsd:positiveInteger } &
+ attribute variable { xsd:nonNegativeInteger }?
+ }?
+}
+
+ClusterParamsV30 =
+ ## Percentage of disk space storage will use storing documents. (Remaining used for resizing)
+ element diskfullpercentage { xsd:nonNegativeInteger { minInclusive = "1" maxInclusive = "99" } }? &
+ ## Whether to restart storage marking failed disks down
+ element faildiskonerror { xsd:boolean }? &
+ ## Time in seconds to keep all operations in case we need to revert them
+ element reverttimeperiod { xsd:nonNegativeInteger }? &
+ ## Time in seconds to keep remove statements for search/VDS recovery
+ element recoveryperiod { xsd:nonNegativeInteger }? &
+ ## Number of bits used for initial splitting.
+ element staticbucketbits { xsd:integer { minInclusive = "16" maxInclusive = "58" } }? &
+ ## Number of bytes per storage file.
+ element bucketsplitsize { xsd:positiveInteger }? &
+ ## Number of documents per storage file.
+ element bucketsplitcount { xsd:positiveInteger }? &
+ PriorityMapping? &
+ element deviceparams {
+ ## Timeout in seconds of a disk operation where storage assumes IO error (0 = never = default)
+ attribute diskoperationtimeout { xsd:nonNegativeInteger }? &
+ ## Not use O_DIRECT when reading from files
+ element cachedio {
+ ## Whether to sync after each VDS operation, default false
+ attribute fsync { xsd:boolean }?
+ }? &
+ ## Disk threads in storage
+ Threads?
+ }? &
+ GarbageCollect? &
+ DeadLockDetector? &
+ Visitors?
+
+FleetControllers = element fleetcontrollers {
+ attribute jvmargs { text }? &
+ FleetControllerStats.attlist &
+ FleetController20+
+}
+
+FleetController20 = element fleetcontroller {
+ service.attlist &
+ attribute index { xsd:nonNegativeInteger }
+}
+
+# Common definitions for v2.0 and v3.0
+
+Metrics = element metrics { element log { text }* }
+
+Disk = element disk {
+ attribute index { xsd:nonNegativeInteger } &
+ attribute capacity { xsd:double { minExclusive = "0.0" } }?
+}
+
+GarbageCollect = element garbagecollect {
+ attribute start { xsd:string { pattern = "[0-9]{2}:[0-9]{2}" } },
+ attribute stop { xsd:string { pattern = "[0-9]{2}:[0-9]{2}" } },
+ attribute full { "monday" | "tuesday" | "wednesday" | "thursday" |
+ "friday" | "saturday" | "sunday" }
+}
+
+DeadLockDetector = element deadlockdetector {
+ attribute enable { xsd:boolean } ? &
+ attribute slack-time-period { xsd:double { minInclusive = "0.0" } }?
+}
+
+FleetController = element fleetcontroller {
+ service.attlist &
+ FleetControllerStats.attlist &
+ attribute index { xsd:nonNegativeInteger } ?
+}
+
+FleetControllerStats.attlist =
+ attribute initprogresstime { xsd:double { minExclusive = "0.0" } } ? &
+ attribute transitiontime { xsd:double { minInclusive = "0.0" } } ? &
+ attribute mintimebeforesettingoutofreachnodedown { xsd:double { minExclusive = "0.0" } } ? &
+ attribute maxprematurecrashes { xsd:nonNegativeInteger } ? &
+ attribute stablestateperiod { xsd:double { minExclusive = "0.0" } } ? &
+ attribute mindistributorupcount { xsd:nonNegativeInteger } ? &
+ attribute mindistributorupratio {
+ xsd:double { minInclusive = "0.0" maxInclusive = "1.0" } } ? &
+ attribute minstorageupcount { xsd:nonNegativeInteger } ? &
+ attribute minstorageupratio {
+ xsd:double { minInclusive = "0.0" maxInclusive = "1.0" } } ? &
+ attribute mintimebetweenclusterstates {
+ xsd:double { minInclusive = "0.0" } } ?
+
+# v3.0 definitions
+Storage30 = element storage {
+ attribute version { "3.0" } &
+ GenericConfig* &
+ element cluster {
+ # name defaults to 'storage'
+ attribute name { xsd:NCName { minLength = "1" } }? &
+ ## Redundancy level - how many copies of each document are made.
+ attribute redundancy { xsd:positiveInteger }? &
+ attribute initial-redundancy { xsd:positiveInteger }? &
+ attribute ensure-primary-persisted { xsd:boolean }? &
+ attribute distributionbits { xsd:positiveInteger } ? &
+ attribute distributorbaseport { xsd:unsignedShort }? &
+ element forwarding { xsd:string }? &
+ GenericConfig* &
+ ( FleetController | FleetControllers ) &
+ ClusterParamsV30? &
+ Metrics? &
+ OperationsLog20? &
+ Tuning20? &
+ Merges? &
+ Datadistribution? &
+ TopStorageGroup
+ }+
+}
+
+TopStorageGroup = element group {
+ attribute index { xsd:nonNegativeInteger }? &
+ attribute name { xsd:string }? &
+ element disks {
+ attribute num { xsd:nonNegativeInteger } &
+ attribute size { xsd:string } &
+ attribute iomodel {xsd:string } &
+ element disk {
+ attribute index { xsd:nonNegativeInteger } &
+ attribute size { xsd:string }
+ } *
+ } ? &
+ (
+ (
+ element distribution {
+ attribute partitions { xsd:string }
+ } &
+ StorageGroup+
+ ) |
+ element node {
+ service.attlist &
+ attribute distributorbaseport { xsd:unsignedShort }? &
+ attribute index { xsd:nonNegativeInteger } &
+ attribute capacity { xsd:double { minExclusive = "0.0" } }?
+ } +
+ )
+ }
+
+StorageGroup = element group {
+ attribute index { xsd:nonNegativeInteger } &
+ attribute name { xsd:string } &
+ element disks {
+ attribute num { xsd:nonNegativeInteger } &
+ attribute size { xsd:string } &
+ attribute iomodel {xsd:string } &
+ element disk {
+ attribute index { xsd:nonNegativeInteger } &
+ attribute size { xsd:string }
+ } *
+ } ? &
+ (
+ (
+ element distribution {
+ attribute partitions { xsd:string }
+ } &
+ StorageGroup+
+ ) |
+ element node {
+ service.attlist &
+ attribute distributorbaseport { xsd:unsignedShort }? &
+ attribute index { xsd:nonNegativeInteger } &
+ attribute capacity { xsd:double { minExclusive = "0.0" } }?
+ } +
+ )
+ }
+
+Merges = element merges {
+ attribute maxpernode { xsd:positiveInteger }? &
+ attribute maxqueuesize { xsd:nonNegativeInteger }?
+}
diff --git a/config-model/src/main/sh/validate-application b/config-model/src/main/sh/validate-application
new file mode 100755
index 00000000000..3af983fbf3d
--- /dev/null
+++ b/config-model/src/main/sh/validate-application
@@ -0,0 +1,87 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+ROOT=$VESPA_HOME
+export ROOT
+
+usage () {
+ echo "usage: `basename $0` <application-directory>"
+ echo " validates the hosts.xml and services.xml in your application"
+}
+
+if [ $# -ne 1 ]; then
+ usage
+ exit 2
+fi
+
+err=$( cd $1 2>&1 1>/dev/null ) || {
+ usage
+ echo "SEVERE: $err"
+ exit 2
+}
+
+SERVICES="$1/services.xml"
+HOSTS="$1/hosts.xml"
+
+
+java -jar $ROOT/lib/jars/jing.jar -c $ROOT/share/vespa/schema/hosts.rnc $HOSTS && echo "$HOSTS is valid"
+java -jar $ROOT/lib/jars/jing.jar -c $ROOT/share/vespa/schema/services.rnc $SERVICES && echo "$SERVICES is valid"
diff --git a/config-model/src/test/cfg/admin/adminconfig20/hosts.xml b/config-model/src/test/cfg/admin/adminconfig20/hosts.xml
new file mode 100644
index 00000000000..0cc1fdf13bf
--- /dev/null
+++ b/config-model/src/test/cfg/admin/adminconfig20/hosts.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>adminserver</alias>
+ <alias>configserver</alias>
+ <alias>logserver</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/admin/adminconfig20/services.xml b/config-model/src/test/cfg/admin/adminconfig20/services.xml
new file mode 100644
index 00000000000..b3a6f3daa7a
--- /dev/null
+++ b/config-model/src/test/cfg/admin/adminconfig20/services.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="configserver" />
+ <logserver hostalias="logserver" />
+ <slobroks>
+ <slobrok hostalias="configserver" />
+ <slobrok hostalias="logserver" />
+ </slobroks>
+ </admin>
+
+</services>
diff --git a/config-model/src/test/cfg/admin/adminconfigbaseport/hosts.xml b/config-model/src/test/cfg/admin/adminconfigbaseport/hosts.xml
new file mode 100644
index 00000000000..8c8d8b7233a
--- /dev/null
+++ b/config-model/src/test/cfg/admin/adminconfigbaseport/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>adminserver</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/admin/adminconfigbaseport/services.xml b/config-model/src/test/cfg/admin/adminconfigbaseport/services.xml
new file mode 100644
index 00000000000..6a44e5f2fe9
--- /dev/null
+++ b/config-model/src/test/cfg/admin/adminconfigbaseport/services.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="adminserver" baseport="9999"/>
+ <adminserver hostalias="adminserver"/>
+ <logserver hostalias="adminserver"/>
+ </admin>
+
+ <search version="1.0">
+ <qrservers>
+ <qrserver hostalias="adminserver" baseport="4080"/>
+ </qrservers>
+ </search>
+
+</services>
diff --git a/config-model/src/test/cfg/admin/metricconfig/hosts.xml b/config-model/src/test/cfg/admin/metricconfig/hosts.xml
new file mode 100644
index 00000000000..6d48a020e93
--- /dev/null
+++ b/config-model/src/test/cfg/admin/metricconfig/hosts.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="foo1">
+ <alias>node1</alias>
+ </host>
+ <host name="foo2">
+ <alias>node2</alias>
+ </host>
+
+</hosts>
diff --git a/config-model/src/test/cfg/admin/metricconfig/searchdefinitions/music.sd b/config-model/src/test/cfg/admin/metricconfig/searchdefinitions/music.sd
new file mode 100644
index 00000000000..3323357cc2f
--- /dev/null
+++ b/config-model/src/test/cfg/admin/metricconfig/searchdefinitions/music.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ header
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ body
+ }
+ }
+}
+
diff --git a/config-model/src/test/cfg/admin/metricconfig/services.xml b/config-model/src/test/cfg/admin/metricconfig/services.xml
new file mode 100644
index 00000000000..cf102f94307
--- /dev/null
+++ b/config-model/src/test/cfg/admin/metricconfig/services.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1"/>
+ <yamas interval="60"/>
+ <metric-consumers>
+ <consumer name="fooConsumer">
+ <metric name="some.foo.metric" output-name="someFooMetric"/>
+ <metric name="some.foo.metric2" output-name="someFooMetric2"/>
+ <metric name="proton.numdocs.average" output-name="tull"/>
+ </consumer>
+ <consumer name="fooConsumer2">
+ <metric name="some.foo.metric3" output-name="someFooMetric3"/>
+ </consumer>
+ <consumer name="yamas">
+ <metric name="vds.distributor.bytesstored.average" output-name="tullball"/>
+ <metric name="proton.numdocs.average" output-name="overridden"/>
+ </consumer>
+ </metric-consumers>
+ </admin>
+
+ <container version="1.0">
+
+ <nodes>
+ <node hostalias="node1"/>
+ <node hostalias="node2" />
+ </nodes>
+
+ <search/>
+
+ <document-api/>
+
+ <document-processing>
+ <chain id="default">
+ <documentprocessor id="com.yahoo.docprocs.FoobarDocumentProcessor"/>
+ </chain>
+ </document-processing>
+
+ </container>
+
+ <content version="1.0" id="music">
+
+ <redundancy>1</redundancy>
+
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+
+ <nodes>
+ <node hostalias="node1" distribution-key="0"/>
+ <node hostalias="node2" distribution-key="1"/>
+ </nodes>
+
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/admin/multipleconfigservers/hosts.xml b/config-model/src/test/cfg/admin/multipleconfigservers/hosts.xml
new file mode 100644
index 00000000000..76b26c00009
--- /dev/null
+++ b/config-model/src/test/cfg/admin/multipleconfigservers/hosts.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+ <host name="localhost2">
+ <alias>node2</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/admin/multipleconfigservers/services.xml b/config-model/src/test/cfg/admin/multipleconfigservers/services.xml
new file mode 100644
index 00000000000..07e3f407d0f
--- /dev/null
+++ b/config-model/src/test/cfg/admin/multipleconfigservers/services.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <configservers>
+ <configserver hostalias="node1" />
+ <configserver hostalias="node2" />
+ </configservers>
+ <logserver hostalias="node2" />
+ </admin>
+
+</services>
diff --git a/config-model/src/test/cfg/admin/sdconfigs/pan-rtx.cfg b/config-model/src/test/cfg/admin/sdconfigs/pan-rtx.cfg
new file mode 100644
index 00000000000..6e18bdf64e7
--- /dev/null
+++ b/config-model/src/test/cfg/admin/sdconfigs/pan-rtx.cfg
@@ -0,0 +1,3 @@
+namespace=config
+foo bar
+baz []678
diff --git a/config-model/src/test/cfg/admin/sdconfigs/partitions.cfg b/config-model/src/test/cfg/admin/sdconfigs/partitions.cfg
new file mode 100644
index 00000000000..20bff56fe4a
--- /dev/null
+++ b/config-model/src/test/cfg/admin/sdconfigs/partitions.cfg
@@ -0,0 +1,2 @@
+namespace=config
+partbits 8
diff --git a/config-model/src/test/cfg/admin/simpleadminconfig20/hosts.xml b/config-model/src/test/cfg/admin/simpleadminconfig20/hosts.xml
new file mode 100644
index 00000000000..8c8d8b7233a
--- /dev/null
+++ b/config-model/src/test/cfg/admin/simpleadminconfig20/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>adminserver</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/admin/simpleadminconfig20/services.xml b/config-model/src/test/cfg/admin/simpleadminconfig20/services.xml
new file mode 100644
index 00000000000..bb77f05086d
--- /dev/null
+++ b/config-model/src/test/cfg/admin/simpleadminconfig20/services.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="adminserver" />
+ </admin>
+
+</services>
diff --git a/config-model/src/test/cfg/admin/userconfigs/function-test.def b/config-model/src/test/cfg/admin/userconfigs/function-test.def
new file mode 100644
index 00000000000..5391ee1dc3c
--- /dev/null
+++ b/config-model/src/test/cfg/admin/userconfigs/function-test.def
@@ -0,0 +1,73 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#
+# This def file should test most aspects of def files that makes a difference
+# for the autogenerated config classes. The goal is to trigger all blocks of
+# code in the code generators. This includes:
+#
+# - Use all legal special characters in the def file name, to ensure that those
+# that needs to be replaced in type names are actually replaced.
+# - Use the same enum type twice to verify that we dont declare or define it
+# twice.
+# - Use the same struct type twice for the same reason.
+# - Include arrays of primitives and structs.
+# - Include enum primitives and array of enums. Arrays of enums must be handled
+# specially by the C++ code.
+# - Include enums both with and without default values.
+# - Include primitive string, numbers & doubles both with and without default
+# values.
+# - Have an array within a struct, to verify that we correctly recurse.
+# - Reuse type name further within to ensure that this works.
+
+namespace=test
+
+# Some random bool without a default value. These comments exist to check
+ # that comment parsing works.
+bool_val bool
+ ## A bool with a default value set.
+bool_with_def bool default=false
+int_val int
+int_with_def int default=-545
+long_val long
+long_with_def long default=-50000000000
+double_val double
+double_with_def double default=-6.43
+# Another comment
+string_val string
+stringwithdef string default="foobar"
+enum_val enum { FOO, BAR, FOOBAR }
+enumwithdef enum { FOO2, BAR2, FOOBAR2 } default=BAR2
+onechoice enum { ONLYFOO } default=ONLYFOO
+refval reference
+refwithdef reference default=":parent:"
+fileVal file
+
+boolarr[] bool
+intarr[] int
+longarr[] long
+doublearr[] double
+stringarr[] string
+enumarr[] enum { ARRAY, VALUES }
+refarr[] reference
+fileArr[] file
+
+# A basic struct
+basicStruct.foo string default="basic"
+basicStruct.bar int
+basicStruct.intArr[] int
+
+# A struct of struct
+rootStruct.inner0.name string default="inner0"
+rootStruct.inner0.index int
+rootStruct.inner1.name string default="inner1"
+rootStruct.inner1.index int
+rootStruct.innerArr[].boolVal bool default=false
+rootStruct.innerArr[].stringVal string
+
+myarray[].intval int default=14
+myarray[].stringval[] string
+myarray[].enumval enum { INNER, ENUM, TYPE } default=TYPE
+myarray[].refval reference # Value in array without default
+myarray[].fileVal file
+myarray[].anotherarray[].foo int default=-4
+myarray[].myStruct.a int
+myarray[].myStruct.b int default=2
diff --git a/config-model/src/test/cfg/admin/userconfigs/functiontest-defaultvalues.xml b/config-model/src/test/cfg/admin/userconfigs/functiontest-defaultvalues.xml
new file mode 100644
index 00000000000..8dc033c7d79
--- /dev/null
+++ b/config-model/src/test/cfg/admin/userconfigs/functiontest-defaultvalues.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<config name="function-test">
+ <bool_val>false</bool_val>
+ <int_val>5</int_val>
+ <long_val>1234567890123</long_val>
+ <double_val>41.23</double_val>
+ <string_val>foo</string_val>
+ <enum_val>FOOBAR</enum_val>
+ <refval>:parent:</refval>
+ <fileVal>vespa.log</fileVal>
+
+ <boolarr><item>false</item>item></boolarr>
+ <doublearr><item>2344</item><item>123</item></doublearr>
+ <stringarr><item>bar</item></stringarr>
+ <enumarr><item>VALUES</item></enumarr>
+
+ <basicStruct>
+ <bar>3</bar>
+ <intArr><item>10</item></intArr>
+ </basicStruct>
+
+ <rootStruct>
+ <inner0>
+ <index>11</index>
+ </inner0>
+ <inner1>
+ <index>12</index>
+ </inner1>
+ <innerArr>
+ <item><stringVal>deep</stringVal></item>item>
+ </innerArr>
+ </rootStruct>
+
+ <myarray>
+ <item>
+ <stringval><item>baah</item><item>yikes</item></stringval>
+ <refval>:parent:</refval>
+ <fileVal>command.com</fileVal>
+ <anotherarray>
+ <item>
+ <foo>7</foo>
+ </item>
+ </anotherarray>
+ <myStruct>
+ <a>1</a>
+ </myStruct>
+ </item>
+ <item>
+ <refval>:parent:</refval>
+ <fileVal>display.sys</fileVal>
+ <anotherarray>
+ <item><foo>1</foo></item>
+ <item><foo>2</foo></item>
+ </anotherarray>
+ <myStruct>
+ <a>-1</a>
+ </myStruct>
+ </item>
+ </myarray>
+
+</config>
diff --git a/config-model/src/test/cfg/admin/userconfigs/statistics.cfg b/config-model/src/test/cfg/admin/userconfigs/statistics.cfg
new file mode 100644
index 00000000000..6ee2fb46670
--- /dev/null
+++ b/config-model/src/test/cfg/admin/userconfigs/statistics.cfg
@@ -0,0 +1,4 @@
+namespace=config
+
+sampleinterval 10000.23
+statinterval -1456
diff --git a/config-model/src/test/cfg/admin/userconfigs/whitespace-test.xml b/config-model/src/test/cfg/admin/userconfigs/whitespace-test.xml
new file mode 100644
index 00000000000..c51dc001626
--- /dev/null
+++ b/config-model/src/test/cfg/admin/userconfigs/whitespace-test.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<config name="function-test">
+ <stringVal> This is a string
+ that contains different kinds of whitespace </stringVal>
+</config>
diff --git a/config-model/src/test/cfg/application/app1/components/defs-only.jar b/config-model/src/test/cfg/application/app1/components/defs-only.jar
new file mode 100644
index 00000000000..c0cf0397c97
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/components/defs-only.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/app1/components/file.txt b/config-model/src/test/cfg/application/app1/components/file.txt
new file mode 100644
index 00000000000..e167ca380f5
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/components/file.txt
@@ -0,0 +1 @@
+/home/vespa/test/file.txt \ No newline at end of file
diff --git a/config-model/src/test/cfg/application/app1/files/foo.json b/config-model/src/test/cfg/application/app1/files/foo.json
new file mode 100644
index 00000000000..ed72b09660a
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/files/foo.json
@@ -0,0 +1 @@
+foo : foo
diff --git a/config-model/src/test/cfg/application/app1/files/sub/bar.json b/config-model/src/test/cfg/application/app1/files/sub/bar.json
new file mode 100644
index 00000000000..2f008f410ec
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/files/sub/bar.json
@@ -0,0 +1 @@
+bar : bar
diff --git a/config-model/src/test/cfg/application/app1/hosts.xml b/config-model/src/test/cfg/application/app1/hosts.xml
new file mode 100644
index 00000000000..fc545b34f6f
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/hosts.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+ <host name="schmocalhost">
+ <alias>node2</alias>
+ </host>
+</hosts>
+
diff --git a/config-model/src/test/cfg/application/app1/searchdefinitions/bar.expression b/config-model/src/test/cfg/application/app1/searchdefinitions/bar.expression
new file mode 100644
index 00000000000..eed496e6aeb
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/searchdefinitions/bar.expression
@@ -0,0 +1 @@
+bar(f*2)
diff --git a/config-model/src/test/cfg/application/app1/searchdefinitions/foo.expression b/config-model/src/test/cfg/application/app1/searchdefinitions/foo.expression
new file mode 100644
index 00000000000..ce26aa75dcb
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/searchdefinitions/foo.expression
@@ -0,0 +1 @@
+foo()+1
diff --git a/config-model/src/test/cfg/application/app1/searchdefinitions/laptop.sd b/config-model/src/test/cfg/application/app1/searchdefinitions/laptop.sd
new file mode 100644
index 00000000000..147e128df16
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/searchdefinitions/laptop.sd
@@ -0,0 +1,41 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search laptop {
+
+ document laptop inherits product {
+
+ field batterycapacity type int {
+ indexing: attribute
+ }
+
+ field location_str type array<string> {
+
+ }
+ }
+
+ field batteryrank type int {
+ indexing: input batterycapacity | attribute
+ }
+
+ field location type array<position> {
+ indexing: input location_str | for_each { to_pos } | attribute
+ }
+
+ rank-profile default {
+ second-phase {
+ expression: fieldMatch(title)*fieldMatch(title).weight
+ rerank-count: 150
+ }
+ summary-features: fieldMatch(title)
+
+ rank-features: attribute(batterycapacity) match.weight.batterycapacity
+
+ rank-properties {
+ fieldMatch(title).maxOccurrences : 40
+ fieldMatch(title).proximityLimit : 5
+ }
+ }
+
+ rank-profile batteryranked {
+ }
+
+}
diff --git a/config-model/src/test/cfg/application/app1/searchdefinitions/music.sd b/config-model/src/test/cfg/application/app1/searchdefinitions/music.sd
new file mode 100644
index 00000000000..d0eec200b90
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/searchdefinitions/music.sd
@@ -0,0 +1,44 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A basic search definition - called music, should be saved to music.sd
+search music {
+
+ # It contains one document type only - called music as well
+ document music {
+
+ field title type string {
+ indexing: summary | index # How this field should be indexed
+ # index-to: title, default # Create two indexes
+ rank-type: about # Type of ranking settings to apply
+ header
+ }
+
+ field artist type string {
+ indexing: summary | attribute | index
+ # index-to: artist, default
+ rank-type:about
+ header
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ header
+ }
+
+ # Increase rank score of popular documents regardless of query
+ field popularity type int {
+ indexing: summary | attribute
+ body
+ }
+
+ field url type uri {
+ indexing: summary | index
+ header
+ }
+
+ field cover type raw {
+ body
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/cfg/application/app1/searchdefinitions/pc.sd b/config-model/src/test/cfg/application/app1/searchdefinitions/pc.sd
new file mode 100644
index 00000000000..89f9ffe530d
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/searchdefinitions/pc.sd
@@ -0,0 +1,47 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search pc {
+
+ document pc inherits product {
+
+ field brand type string {
+ indexing: index | summary
+ }
+
+ field color type string {
+ indexing: summary | index
+ index: prefix
+ alias: colour
+ rank: filter
+ }
+
+ field cpuspeed type int {
+ indexing: summary
+ }
+
+ field location_str type array<string> {
+
+ }
+ }
+
+ field location type array<position> {
+ indexing: input location_str | for_each { to_pos } | attribute
+ }
+
+ rank-profile default {
+ first-phase {
+ expression: fieldMatch(brand).completeness + fieldMatch(color).completeness
+ }
+ second-phase {
+ expression: fieldMatch(brand).completeness*fieldMatch(brand).importancy + fieldMatch(color).completeness*fieldMatch(color).importancy
+ }
+
+ summary-features: fieldMatch(title) fieldMatch(brand).proximity match.weight.title nativeFieldMatch(title)
+
+ rank-features: attribute(cpuspeed)
+
+ rank-properties {
+ fieldMatch(brand).maxOccurrences : 20
+ }
+ }
+
+}
diff --git a/config-model/src/test/cfg/application/app1/searchdefinitions/product.sd b/config-model/src/test/cfg/application/app1/searchdefinitions/product.sd
new file mode 100644
index 00000000000..d8b1d725d1c
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/searchdefinitions/product.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document product {
+
+ field title type string {
+ indexing: index | summary
+ # index-to: title, default
+ }
+
+ field price type int {
+ indexing: index | summary | attribute
+ }
+
+}
diff --git a/config-model/src/test/cfg/application/app1/searchdefinitions/sock.sd b/config-model/src/test/cfg/application/app1/searchdefinitions/sock.sd
new file mode 100644
index 00000000000..1620d790b65
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/searchdefinitions/sock.sd
@@ -0,0 +1,27 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search sock {
+
+ document sock inherits product {
+
+ field size type int {
+ indexing: index | summary | attribute
+ }
+
+ field color type string {
+ indexing: summary
+ index: prefix
+ }
+
+ field brand type string {
+ indexing: summary
+ }
+
+ }
+
+ rank-profile other {
+ second-phase {
+ expression: fieldMatch(color).fieldCompleteness + fieldMatch(brand).proximity
+ }
+ }
+
+}
diff --git a/config-model/src/test/cfg/application/app1/services.xml b/config-model/src/test/cfg/application/app1/services.xml
new file mode 100644
index 00000000000..537ec488537
--- /dev/null
+++ b/config-model/src/test/cfg/application/app1/services.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <slobroks>
+ <slobrok hostalias="node1"/>
+ <slobrok hostalias="node2"/>
+ </slobroks>
+ </admin>
+
+ <container version="1.0">
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+
+ <search/>
+ <document-api/>
+ </container>
+
+ <content id="music" version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index" />
+ </documents>
+ <nodes>
+ <node hostalias="node1" distribution-key="0" />
+ </nodes>
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/application/app_genericservices/hosts.xml b/config-model/src/test/cfg/application/app_genericservices/hosts.xml
new file mode 100644
index 00000000000..7a41bc218cd
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_genericservices/hosts.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="bogusname1">
+ <alias>node1</alias>
+ </host>
+ <host name="bogusname2">
+ <alias>node2</alias>
+ </host>
+ <host name="bogusname3">
+ <alias>node3</alias>
+ </host>
+ <host name="bogusname4">
+ <alias>node4</alias>
+ </host>
+ <host name="bogusname5">
+ <alias>node5</alias>
+ </host>
+
+</hosts>
+
diff --git a/config-model/src/test/cfg/application/app_genericservices/searchdefinitions/music.sd b/config-model/src/test/cfg/application/app_genericservices/searchdefinitions/music.sd
new file mode 100644
index 00000000000..d0eec200b90
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_genericservices/searchdefinitions/music.sd
@@ -0,0 +1,44 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A basic search definition - called music, should be saved to music.sd
+search music {
+
+ # It contains one document type only - called music as well
+ document music {
+
+ field title type string {
+ indexing: summary | index # How this field should be indexed
+ # index-to: title, default # Create two indexes
+ rank-type: about # Type of ranking settings to apply
+ header
+ }
+
+ field artist type string {
+ indexing: summary | attribute | index
+ # index-to: artist, default
+ rank-type:about
+ header
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ header
+ }
+
+ # Increase rank score of popular documents regardless of query
+ field popularity type int {
+ indexing: summary | attribute
+ body
+ }
+
+ field url type uri {
+ indexing: summary | index
+ header
+ }
+
+ field cover type raw {
+ body
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/cfg/application/app_genericservices/services.xml b/config-model/src/test/cfg/application/app_genericservices/services.xml
new file mode 100644
index 00000000000..67169253bba
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_genericservices/services.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <service version="1.0" name="myservice" command="mycmd1.sh">
+ <config name="myconfig">
+ <mysetting>bar</mysetting>
+ </config>
+ <node hostalias="node1">
+ <config name="myconfig">
+ <mysetting>baz</mysetting>
+ </config>
+ </node>
+ <node hostalias="node2"/>
+ <node hostalias="node3"/>
+ <node hostalias="node3"/>
+ </service>
+
+ <service version="1.0" name="myotherservice" command="/home/vespa/bin/mycmd2.sh --ytest $FOO_BAR">
+ <config name="myconfig">
+ <mysetting>bar2</mysetting>
+ </config>
+ <node hostalias="node3">
+ <config name="myconfig">
+ <mysetting>baz2</mysetting>
+ </config>
+ </node>
+ <node hostalias="node4"/>
+ <node hostalias="node5"/>
+ </service>
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <slobroks>
+ <slobrok hostalias="node1"/>
+ <slobrok hostalias="node2"/>
+ </slobroks>
+ </admin>
+
+ <container version="1.0">
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+
+ <search/>
+ <document-api/>
+ </container>
+
+ <content id="music" version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index" />
+ </documents>
+ <nodes>
+ <node hostalias="node1" distribution-key="0" />
+ </nodes>
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mail.sd b/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mail.sd
new file mode 100644
index 00000000000..6ce2f4164e8
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mail.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search mail {
+
+ document mail {
+
+ }
+
+}
+
diff --git a/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mailbox.sd b/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mailbox.sd
new file mode 100644
index 00000000000..fac4abe46a8
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/mailbox.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search mailbox {
+
+ document mailbox {
+ }
+
+
+}
+
diff --git a/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/message.sd b/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/message.sd
new file mode 100644
index 00000000000..b31b2055dd5
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_nohosts/searchdefinitions/message.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search message {
+
+ document message {
+
+ }
+
+}
+
diff --git a/config-model/src/test/cfg/application/app_nohosts/services.xml b/config-model/src/test/cfg/application/app_nohosts/services.xml
new file mode 100644
index 00000000000..3169394f9d4
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_nohosts/services.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="ADMIN0" />
+ <logserver hostalias="ADMIN0" />
+ <slobroks>
+ <slobrok hostalias="SLOBROK0" />
+ </slobroks>
+ <configservers>
+ <configserver hostalias="ADMIN0" />
+ <configserver hostalias="SLOBROK0" />
+ </configservers>
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <route name="cats" hops="storage"/>
+ </routingtable>
+ </routing>
+
+ <container version="1.0">
+ <nodes>
+ <node hostalias="QRS0"/>
+ </nodes>
+ <document-api/>
+ <handler id="com.yahoo.foo.bar.FooHandler:1" />
+ </container>
+
+ <container version="1.0" id="migration">
+ <http>
+ <server port="8000" id="migration-server" />
+ </http>
+ <document-processing>
+ <chain id="baz">
+ <documentprocessor id="com.yahoo.foo.bar.baz.BazDocproc" />
+ </chain>
+ </document-processing>
+
+ <nodes>
+ <node hostalias="DOCPROC0" />
+ </nodes>
+ </container>
+
+ <content version="1.0" id="mailbox">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="mailbox" mode="streaming"/>
+ </documents>
+ <nodes><node hostalias="VDS0" distribution-key="0"/></nodes>
+ </content>
+
+ <content version="1.0" id="message">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="message" mode="streaming"/>
+ </documents>
+ <nodes><node hostalias="VDS0" distribution-key="0"/></nodes>
+ </content>
+
+ <content version="1.0" id="mail">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="mail" mode="streaming"/>
+ </documents>
+ <nodes><node hostalias="VDS0" distribution-key="0"/></nodes>
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/application/app_permanent/permanent-services.xml b/config-model/src/test/cfg/application/app_permanent/permanent-services.xml
new file mode 100644
index 00000000000..85ae636d206
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_permanent/permanent-services.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+ <jdisc version="1.0">
+ <search />
+ </jdisc>
+</services>
diff --git a/config-model/src/test/cfg/application/app_qrserverandgw/hosts.xml b/config-model/src/test/cfg/application/app_qrserverandgw/hosts.xml
new file mode 100644
index 00000000000..346dc5c7652
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_qrserverandgw/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node0</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/app_qrserverandgw/searchdefinitions/message.sd b/config-model/src/test/cfg/application/app_qrserverandgw/searchdefinitions/message.sd
new file mode 100644
index 00000000000..fdc220aef3d
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_qrserverandgw/searchdefinitions/message.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search message {
+
+ document message {
+ field foo type string {
+ indexing: summary
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/application/app_qrserverandgw/services.xml b/config-model/src/test/cfg/application/app_qrserverandgw/services.xml
new file mode 100644
index 00000000000..5136b06b892
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_qrserverandgw/services.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node0"/>
+ </admin>
+
+ <container version="1.0">
+ <nodes>
+ <node hostalias="node0"/>
+ </nodes>
+ <document-api/>
+ <search/>
+ </container>
+
+ <content version="1.0" id="message">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="message" mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias="node0" distribution-key="0" />
+ </nodes>
+ </content>
+
+
+</services>
diff --git a/config-model/src/test/cfg/application/app_sdbundles/components/testbundle.jar b/config-model/src/test/cfg/application/app_sdbundles/components/testbundle.jar
new file mode 100644
index 00000000000..00749d776c2
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_sdbundles/components/testbundle.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/app_sdbundles/components/testbundle2.jar b/config-model/src/test/cfg/application/app_sdbundles/components/testbundle2.jar
new file mode 100644
index 00000000000..36c97c2716c
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_sdbundles/components/testbundle2.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/app_sdbundles/files/foo.txt b/config-model/src/test/cfg/application/app_sdbundles/files/foo.txt
new file mode 100644
index 00000000000..b7d6715e2df
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_sdbundles/files/foo.txt
@@ -0,0 +1 @@
+FOO
diff --git a/config-model/src/test/cfg/application/app_sdbundles/files/subdir/bar.txt b/config-model/src/test/cfg/application/app_sdbundles/files/subdir/bar.txt
new file mode 100644
index 00000000000..ba578e48b18
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_sdbundles/files/subdir/bar.txt
@@ -0,0 +1 @@
+BAR
diff --git a/config-model/src/test/cfg/application/app_sdbundles/hosts.xml b/config-model/src/test/cfg/application/app_sdbundles/hosts.xml
new file mode 100644
index 00000000000..fc545b34f6f
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_sdbundles/hosts.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+ <host name="schmocalhost">
+ <alias>node2</alias>
+ </host>
+</hosts>
+
diff --git a/config-model/src/test/cfg/application/app_sdbundles/services.xml b/config-model/src/test/cfg/application/app_sdbundles/services.xml
new file mode 100644
index 00000000000..537ec488537
--- /dev/null
+++ b/config-model/src/test/cfg/application/app_sdbundles/services.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <slobroks>
+ <slobrok hostalias="node1"/>
+ <slobrok hostalias="node2"/>
+ </slobroks>
+ </admin>
+
+ <container version="1.0">
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+
+ <search/>
+ <document-api/>
+ </container>
+
+ <content id="music" version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index" />
+ </documents>
+ <nodes>
+ <node hostalias="node1" distribution-key="0" />
+ </nodes>
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/application/classes/attributes.def b/config-model/src/test/cfg/application/classes/attributes.def
new file mode 100644
index 00000000000..bb3a0df6299
--- /dev/null
+++ b/config-model/src/test/cfg/application/classes/attributes.def
@@ -0,0 +1,7 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+attribute[].name string
+attribute[].datatype string
+attribute[].multivalue bool default=false
+attribute[].sortsigned bool default=true
+attribute[].disableprep bool default=false
diff --git a/config-model/src/test/cfg/application/com/yahoo/vespa/model/test/.gitignore b/config-model/src/test/cfg/application/com/yahoo/vespa/model/test/.gitignore
new file mode 100644
index 00000000000..6ff331c7e35
--- /dev/null
+++ b/config-model/src/test/cfg/application/com/yahoo/vespa/model/test/.gitignore
@@ -0,0 +1 @@
+hosts
diff --git a/config-model/src/test/cfg/application/components/com.yahoo.searcher1.jar b/config-model/src/test/cfg/application/components/com.yahoo.searcher1.jar
new file mode 100644
index 00000000000..437246152db
--- /dev/null
+++ b/config-model/src/test/cfg/application/components/com.yahoo.searcher1.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/configdeftest/configdefinitions/bar.def b/config-model/src/test/cfg/application/configdeftest/configdefinitions/bar.def
new file mode 100644
index 00000000000..5ba447b9f7d
--- /dev/null
+++ b/config-model/src/test/cfg/application/configdeftest/configdefinitions/bar.def
@@ -0,0 +1,3 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=xyzzy
+bar int default="1"
diff --git a/config-model/src/test/cfg/application/configdeftest/configdefinitions/baz.def b/config-model/src/test/cfg/application/configdeftest/configdefinitions/baz.def
new file mode 100644
index 00000000000..d9649a64203
--- /dev/null
+++ b/config-model/src/test/cfg/application/configdeftest/configdefinitions/baz.def
@@ -0,0 +1,3 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=xyzzy
+bar int
diff --git a/config-model/src/test/cfg/application/configdeftest/configdefinitions/foo.def b/config-model/src/test/cfg/application/configdeftest/configdefinitions/foo.def
new file mode 100644
index 00000000000..f1cb7a75c8a
--- /dev/null
+++ b/config-model/src/test/cfg/application/configdeftest/configdefinitions/foo.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=2
+namespace=config
+bar int default=1
diff --git a/config-model/src/test/cfg/application/configdeftest/configdefinitions/qux.foo.def b/config-model/src/test/cfg/application/configdeftest/configdefinitions/qux.foo.def
new file mode 100644
index 00000000000..5fa36afc8e4
--- /dev/null
+++ b/config-model/src/test/cfg/application/configdeftest/configdefinitions/qux.foo.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=qux
+bar int default=2
+quux int default=3
diff --git a/config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.bar.def b/config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.bar.def
new file mode 100644
index 00000000000..aae291ec190
--- /dev/null
+++ b/config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.bar.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=xyzzy
+bar int default="2"
+foo int
diff --git a/config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.def b/config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.def
new file mode 100644
index 00000000000..e078d9cf77a
--- /dev/null
+++ b/config-model/src/test/cfg/application/configdeftest/configdefinitions/xyzzy.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config
+bar int default=1
diff --git a/config-model/src/test/cfg/application/configuredportconfig/hosts.xml b/config-model/src/test/cfg/application/configuredportconfig/hosts.xml
new file mode 100644
index 00000000000..f3b3ad44df1
--- /dev/null
+++ b/config-model/src/test/cfg/application/configuredportconfig/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/configuredportconfig/services.xml b/config-model/src/test/cfg/application/configuredportconfig/services.xml
new file mode 100644
index 00000000000..1b85de210df
--- /dev/null
+++ b/config-model/src/test/cfg/application/configuredportconfig/services.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <com.yahoo.vespa.model.test.SimplePlugin version="1.0">
+ <simpleservice hostalias="host1" />
+ <simpleservice hostalias="host1" />
+ <simpleservice hostalias="host1" baseport="9000" />
+
+ <!-- Conflicts with the previous service -->
+ <simpleservice hostalias="host1" baseport="9000" />
+
+ <!-- Conflicts with the first service, because of getWantedPort() -->
+ <simpleservice hostalias="host1" baseport="10000" />
+
+ <!-- Above the dynamic port range -->
+ <simpleservice hostalias="host1" baseport="20000" />
+
+ </com.yahoo.vespa.model.test.SimplePlugin>
+</services>
diff --git a/config-model/src/test/cfg/application/custompropconfig/hosts.xml b/config-model/src/test/cfg/application/custompropconfig/hosts.xml
new file mode 100644
index 00000000000..02dc37701cf
--- /dev/null
+++ b/config-model/src/test/cfg/application/custompropconfig/hosts.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ <alias>host2</alias>
+ <alias>host3</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/custompropconfig/services.xml b/config-model/src/test/cfg/application/custompropconfig/services.xml
new file mode 100644
index 00000000000..229dfdf7f06
--- /dev/null
+++ b/config-model/src/test/cfg/application/custompropconfig/services.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <com.yahoo.vespa.model.test.SimpleCustomPropPlugin version="1.0">
+ <simplecustompropservice hostalias="host1" />
+ </com.yahoo.vespa.model.test.SimpleCustomPropPlugin>
+</services>
diff --git a/config-model/src/test/cfg/application/doubleconfig/hosts.xml b/config-model/src/test/cfg/application/doubleconfig/hosts.xml
new file mode 100644
index 00000000000..02dc37701cf
--- /dev/null
+++ b/config-model/src/test/cfg/application/doubleconfig/hosts.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ <alias>host2</alias>
+ <alias>host3</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/doubleconfig/services.xml b/config-model/src/test/cfg/application/doubleconfig/services.xml
new file mode 100644
index 00000000000..74efd598d01
--- /dev/null
+++ b/config-model/src/test/cfg/application/doubleconfig/services.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <simpleplugin version="1.0">
+ <simpleservice hostalias="host2" />
+ <simpleservice hostalias="host2" />
+ </simpleplugin>
+
+ <simpleplugin version="1.0">
+ <simpleservice hostalias="host2" />
+ </simpleplugin>
+
+</services>
diff --git a/config-model/src/test/cfg/application/include_dirs/dir1/default.xml b/config-model/src/test/cfg/application/include_dirs/dir1/default.xml
new file mode 100644
index 00000000000..f1e16333fc1
--- /dev/null
+++ b/config-model/src/test/cfg/application/include_dirs/dir1/default.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="default">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ </chain>
+</search>
diff --git a/config-model/src/test/cfg/application/include_dirs/dir2/chain2.xml b/config-model/src/test/cfg/application/include_dirs/dir2/chain2.xml
new file mode 100644
index 00000000000..9d297be5212
--- /dev/null
+++ b/config-model/src/test/cfg/application/include_dirs/dir2/chain2.xml
@@ -0,0 +1,8 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <searcher class="com.yahoo.search.example.SimpleSearcher" id="s1" bundle="mybundle"/>
+ <chain id="chain2">
+ <searcher id="s1"/>
+ <searcher id="com.yahoo.search.example.SimpleSearcher2" bundle="mybundle"/>
+ </chain>
+</search>
diff --git a/config-model/src/test/cfg/application/include_dirs/dir2/chain3.xml b/config-model/src/test/cfg/application/include_dirs/dir2/chain3.xml
new file mode 100644
index 00000000000..0e019ba9d02
--- /dev/null
+++ b/config-model/src/test/cfg/application/include_dirs/dir2/chain3.xml
@@ -0,0 +1,10 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="chain3_1">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ </chain>
+ <chain id="chain3_2">
+ <searcher id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ <searcher id="com.yahoo.search.example.SimpleSearcher2" bundle="mybundle"/>
+ </chain>
+</search>
diff --git a/config-model/src/test/cfg/application/include_dirs/empty_dir/.gitignore b/config-model/src/test/cfg/application/include_dirs/empty_dir/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/application/include_dirs/empty_dir/.gitignore
diff --git a/config-model/src/test/cfg/application/include_dirs/jdisc_dir/jdisc1.xml b/config-model/src/test/cfg/application/include_dirs/jdisc_dir/jdisc1.xml
new file mode 100644
index 00000000000..629fc830372
--- /dev/null
+++ b/config-model/src/test/cfg/application/include_dirs/jdisc_dir/jdisc1.xml
@@ -0,0 +1,4 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<components>
+ <component id="test.Exampledocproc"/>
+</components>
diff --git a/config-model/src/test/cfg/application/include_dirs/services.xml b/config-model/src/test/cfg/application/include_dirs/services.xml
new file mode 100644
index 00000000000..c7349881aed
--- /dev/null
+++ b/config-model/src/test/cfg/application/include_dirs/services.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+
+ <jdisc id="default" version="1.0">
+ <!-- <component id="test.Exampledocproc"/> -->
+ <components>
+ <include dir="jdisc_dir"/>
+ </components>
+ <search>
+ <include dir='dir1'/>
+ <include dir='dir2'/>
+ <include dir='empty_dir'/>
+ </search>
+
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+ </jdisc>
+
+</services>
diff --git a/config-model/src/test/cfg/application/invalid_legacy_user_config/configs/qr-searchers.cfg b/config-model/src/test/cfg/application/invalid_legacy_user_config/configs/qr-searchers.cfg
new file mode 100644
index 00000000000..07538a1dce2
--- /dev/null
+++ b/config-model/src/test/cfg/application/invalid_legacy_user_config/configs/qr-searchers.cfg
@@ -0,0 +1,5 @@
+builtin[StemmingSearcher].enabled false
+builtin[NoRankingSearcher].enabled false
+tag.bold.open "^_"
+tag.bold.close "^_"
+tag.separator " ... "
diff --git a/config-model/src/test/cfg/application/invalid_legacy_user_config/services.xml b/config-model/src/test/cfg/application/invalid_legacy_user_config/services.xml
new file mode 100644
index 00000000000..2cfe35d8893
--- /dev/null
+++ b/config-model/src/test/cfg/application/invalid_legacy_user_config/services.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+</services>
diff --git a/config-model/src/test/cfg/application/metricsconfig/hosts.xml b/config-model/src/test/cfg/application/metricsconfig/hosts.xml
new file mode 100644
index 00000000000..02dc37701cf
--- /dev/null
+++ b/config-model/src/test/cfg/application/metricsconfig/hosts.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ <alias>host2</alias>
+ <alias>host3</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/metricsconfig/services.xml b/config-model/src/test/cfg/application/metricsconfig/services.xml
new file mode 100644
index 00000000000..dd6005f1e74
--- /dev/null
+++ b/config-model/src/test/cfg/application/metricsconfig/services.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <simple version="1.0">
+ <simpleservice hostalias="host2">
+ <metric-consumers>
+ <consumer name="yamas">
+ <metric name="onlyMine" output-name="user"/>
+ </consumer>
+ </metric-consumers>
+ </simpleservice>
+
+ <simpleservice hostalias="host2">
+ <metric-consumers>
+ <consumer name="yamas">
+ <metric name="test" output-name="user"/>
+ </consumer>
+ </metric-consumers>
+ </simpleservice>
+ </simple>
+</services>
diff --git a/config-model/src/test/cfg/application/newfilenames/hosts.xml b/config-model/src/test/cfg/application/newfilenames/hosts.xml
new file mode 100644
index 00000000000..02dc37701cf
--- /dev/null
+++ b/config-model/src/test/cfg/application/newfilenames/hosts.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ <alias>host2</alias>
+ <alias>host3</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/newfilenames/services.xml b/config-model/src/test/cfg/application/newfilenames/services.xml
new file mode 100644
index 00000000000..f76ab8eba78
--- /dev/null
+++ b/config-model/src/test/cfg/application/newfilenames/services.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <simpleplugin version="1.0">
+ <simpleservice hostalias="host2" />
+ <simpleservice hostalias="host2" />
+ </simpleplugin>
+</services>
diff --git a/config-model/src/test/cfg/application/plugins/hosts.xml b/config-model/src/test/cfg/application/plugins/hosts.xml
new file mode 100644
index 00000000000..f3b3ad44df1
--- /dev/null
+++ b/config-model/src/test/cfg/application/plugins/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/plugins/services.xml b/config-model/src/test/cfg/application/plugins/services.xml
new file mode 100644
index 00000000000..79aa2ab9740
--- /dev/null
+++ b/config-model/src/test/cfg/application/plugins/services.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="host1" />
+ </admin>
+
+ <simple version="1.0">
+ <simpleservice hostalias="host1" />
+ <simpleservice hostalias="host1" />
+ </simple>
+
+ <simple id="simple2" version="1.0">
+ <simpleservice hostalias="host1" />
+ </simple>
+
+ <api version="1.0">
+ <apiservice hostalias="host1" />
+ </api>
+
+</services>
diff --git a/config-model/src/test/cfg/application/sdfilenametest/searchdefinitions/notmusic.sd b/config-model/src/test/cfg/application/sdfilenametest/searchdefinitions/notmusic.sd
new file mode 100644
index 00000000000..a70236a7d6f
--- /dev/null
+++ b/config-model/src/test/cfg/application/sdfilenametest/searchdefinitions/notmusic.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document music {
+
+ field title type string {
+ indexing: summary | index
+ # index-to: title, default
+ }
+ }
+
+}
diff --git a/config-model/src/test/cfg/application/sdfilenametest/services.xml b/config-model/src/test/cfg/application/sdfilenametest/services.xml
new file mode 100644
index 00000000000..1052e27430f
--- /dev/null
+++ b/config-model/src/test/cfg/application/sdfilenametest/services.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+</services>
diff --git a/config-model/src/test/cfg/application/serverdefs/attributes.def b/config-model/src/test/cfg/application/serverdefs/attributes.def
new file mode 100644
index 00000000000..aa9bce983ab
--- /dev/null
+++ b/config-model/src/test/cfg/application/serverdefs/attributes.def
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+
+attribute[].name string
+attribute[].datatype string
+attribute[].multivalue bool default=false
+attribute[].sortsigned bool default=true
+attribute[].disableprep bool default=false
diff --git a/config-model/src/test/cfg/application/simpleconfig/hosts.xml b/config-model/src/test/cfg/application/simpleconfig/hosts.xml
new file mode 100644
index 00000000000..02dc37701cf
--- /dev/null
+++ b/config-model/src/test/cfg/application/simpleconfig/hosts.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ <alias>host2</alias>
+ <alias>host3</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/simpleconfig/services.xml b/config-model/src/test/cfg/application/simpleconfig/services.xml
new file mode 100644
index 00000000000..25b5057fe8b
--- /dev/null
+++ b/config-model/src/test/cfg/application/simpleconfig/services.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <simple version="1.0">
+ <simpleservice hostalias="host2" />
+ <simpleservice hostalias="host2" />
+ </simple>
+
+ <simple id="second" version="1.0">
+ <simpleservice hostalias="host3" />
+ </simple>
+
+</services>
diff --git a/config-model/src/test/cfg/application/treeconfig/hosts.xml b/config-model/src/test/cfg/application/treeconfig/hosts.xml
new file mode 100644
index 00000000000..f3b3ad44df1
--- /dev/null
+++ b/config-model/src/test/cfg/application/treeconfig/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>host1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/treeconfig/services.xml b/config-model/src/test/cfg/application/treeconfig/services.xml
new file mode 100644
index 00000000000..cee45284c2e
--- /dev/null
+++ b/config-model/src/test/cfg/application/treeconfig/services.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <com.yahoo.vespa.model.test.SimplePlugin version="1.0">
+ <parentservice hostalias="host1" >
+ <simpleservice hostalias="host1" />
+ <simpleservice hostalias="host1" />
+ <simpleservice hostalias="host1" />
+
+ <parentservice hostalias="host1" >
+ <simpleservice hostalias="host1" />
+ <simpleservice hostalias="host1" />
+ </parentservice>
+
+ </parentservice>
+
+ <parentservice hostalias="host1" />
+
+ </com.yahoo.vespa.model.test.SimplePlugin>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/components/.gitignore b/config-model/src/test/cfg/application/validation/components/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/components/.gitignore
diff --git a/config-model/src/test/cfg/application/validation/index_struct/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/index_struct/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..d13477732d8
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/index_struct/searchdefinitions/simple.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field foo type map<string, string> { }
+ field bar type map<string, string> { indexing: summary}
+ field baz type map<string, string> { indexing: index | summary }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/index_struct/services.xml b/config-model/src/test/cfg/application/validation/index_struct/services.xml
new file mode 100644
index 00000000000..be6672f415f
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/index_struct/services.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/invalidjar_app/components/invalid.jar b/config-model/src/test/cfg/application/validation/invalidjar_app/components/invalid.jar
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/invalidjar_app/components/invalid.jar
diff --git a/config-model/src/test/cfg/application/validation/prefix/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/prefix/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..65f0502a8f7
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix/searchdefinitions/simple.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field artist type string {
+ indexing: summary | attribute
+ match:prefix
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/prefix/services.xml b/config-model/src/test/cfg/application/validation/prefix/services.xml
new file mode 100644
index 00000000000..2518a2cc4f0
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix/services.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/prefix_index/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/prefix_index/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..7dc6b88c037
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix_index/searchdefinitions/simple.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field artist type string {
+ indexing: summary | index
+ match:prefix
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/prefix_index/services.xml b/config-model/src/test/cfg/application/validation/prefix_index/services.xml
new file mode 100644
index 00000000000..2518a2cc4f0
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix_index/services.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/prefix_index_and_attribute/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/prefix_index_and_attribute/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..4fea7a5c3a4
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix_index_and_attribute/searchdefinitions/simple.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field artist type string {
+ indexing: summary | attribute | index
+ match:prefix
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/prefix_index_and_attribute/services.xml b/config-model/src/test/cfg/application/validation/prefix_index_and_attribute/services.xml
new file mode 100644
index 00000000000..2518a2cc4f0
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix_index_and_attribute/services.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/prefix_streaming/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/prefix_streaming/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..7dc6b88c037
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix_streaming/searchdefinitions/simple.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field artist type string {
+ indexing: summary | index
+ match:prefix
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/prefix_streaming/services.xml b/config-model/src/test/cfg/application/validation/prefix_streaming/services.xml
new file mode 100644
index 00000000000..4700fa75612
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/prefix_streaming/services.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="streaming"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/search_alltypes/hosts.xml b/config-model/src/test/cfg/application/validation/search_alltypes/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_alltypes/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/validation/search_alltypes/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/search_alltypes/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..d2851816bc0
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_alltypes/searchdefinitions/simple.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field my_pos type position { indexing: summary }
+ field my_int type int { indexing: summary }
+ field my_float type float { indexing: summary }
+ field my_str type string { indexing: summary }
+ field my_raw type raw { indexing: summary }
+ field my_long type long { indexing: summary }
+ field my_double type double { indexing: summary }
+ field my_uri type uri { indexing: summary }
+ field my_byte type byte { indexing: summary }
+ field my_predicate type predicate { indexing: summary }
+ field my_tensor type tensor { indexing: summary }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/search_alltypes/services.xml b/config-model/src/test/cfg/application/validation/search_alltypes/services.xml
new file mode 100644
index 00000000000..be6672f415f
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_alltypes/services.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/search_content/hosts.xml b/config-model/src/test/cfg/application/validation/search_content/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_content/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/validation/search_content/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/search_content/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..3647689de10
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_content/searchdefinitions/simple.sd
@@ -0,0 +1,7 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field foo type raw { }
+ field bar type raw { indexing: summary }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/search_content/services.xml b/config-model/src/test/cfg/application/validation/search_content/services.xml
new file mode 100644
index 00000000000..c415b2aad4c
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_content/services.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+ <search version="2.0">
+ <qrservers>
+ <qrserver hostalias="node1" />
+ </qrservers>
+ <cluster indexingmode="realtime">
+ <searchdefinitions>
+ <searchdefinition name="simple" />
+ </searchdefinitions>
+ <clustercontrollers>
+ <clustercontroller hostalias="node1" />
+ </clustercontrollers>
+ <topleveldispatchers>
+ <topleveldispatcher hostalias="node1" />
+ </topleveldispatchers>
+ <row index="0">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0" />
+ </searchnodes>
+ </row>
+ </cluster>
+ </search>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/search_empty_content/hosts.xml b/config-model/src/test/cfg/application/validation/search_empty_content/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_empty_content/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/validation/search_empty_content/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/search_empty_content/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..023943fd3b1
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_empty_content/searchdefinitions/simple.sd
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field foo type raw { }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/search_empty_content/services.xml b/config-model/src/test/cfg/application/validation/search_empty_content/services.xml
new file mode 100644
index 00000000000..be6672f415f
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_empty_content/services.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/search_struct/hosts.xml b/config-model/src/test/cfg/application/validation/search_struct/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_struct/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/application/validation/search_struct/searchdefinitions/simple.sd b/config-model/src/test/cfg/application/validation/search_struct/searchdefinitions/simple.sd
new file mode 100644
index 00000000000..e8d3d37906d
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_struct/searchdefinitions/simple.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field foo type my_struct { }
+ field bar type my_struct { indexing: summary }
+ struct my_struct {
+ field bar type string { }
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/application/validation/search_struct/services.xml b/config-model/src/test/cfg/application/validation/search_struct/services.xml
new file mode 100644
index 00000000000..be6672f415f
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/search_struct/services.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+ <content version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type='simple' mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias='node1' distribution-key='0'/>
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/application/validation/testjars/missing_osgi_headers.jar b/config-model/src/test/cfg/application/validation/testjars/missing_osgi_headers.jar
new file mode 100644
index 00000000000..84781c4802e
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/testjars/missing_osgi_headers.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/validation/testjars/nomanifest.jar b/config-model/src/test/cfg/application/validation/testjars/nomanifest.jar
new file mode 100644
index 00000000000..f4f7dd4e127
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/testjars/nomanifest.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/validation/testjars/ok.jar b/config-model/src/test/cfg/application/validation/testjars/ok.jar
new file mode 100644
index 00000000000..fce043c6ff7
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/testjars/ok.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/validation/testjars/snapshot_bundle.jar b/config-model/src/test/cfg/application/validation/testjars/snapshot_bundle.jar
new file mode 100644
index 00000000000..a395a52d17d
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/testjars/snapshot_bundle.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/validation/testjars/test.jar b/config-model/src/test/cfg/application/validation/testjars/test.jar
new file mode 100644
index 00000000000..47fbd01f1ec
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/testjars/test.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/validation/testjars/wrong_classpath.jar b/config-model/src/test/cfg/application/validation/testjars/wrong_classpath.jar
new file mode 100644
index 00000000000..31266f1e8f2
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/testjars/wrong_classpath.jar
Binary files differ
diff --git a/config-model/src/test/cfg/application/validation/testjars/wrong_export.jar b/config-model/src/test/cfg/application/validation/testjars/wrong_export.jar
new file mode 100644
index 00000000000..47fbd01f1ec
--- /dev/null
+++ b/config-model/src/test/cfg/application/validation/testjars/wrong_export.jar
Binary files differ
diff --git a/config-model/src/test/cfg/clients/advancedconfig.v2/hosts.xml b/config-model/src/test/cfg/clients/advancedconfig.v2/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/clients/advancedconfig.v2/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/clients/advancedconfig.v2/searchdefinitions/music.sd b/config-model/src/test/cfg/clients/advancedconfig.v2/searchdefinitions/music.sd
new file mode 100644
index 00000000000..136efeafaf6
--- /dev/null
+++ b/config-model/src/test/cfg/clients/advancedconfig.v2/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/clients/advancedconfig.v2/services.xml b/config-model/src/test/cfg/clients/advancedconfig.v2/services.xml
new file mode 100644
index 00000000000..5147b1b546f
--- /dev/null
+++ b/config-model/src/test/cfg/clients/advancedconfig.v2/services.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services xmlns="">
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ <slobroks>
+ <slobrok hostalias="node1" />
+ </slobroks>
+ </admin>
+
+ <clients version="2.0">
+ <spoolers>
+ <feederoptions>
+ <timeout>90</timeout>
+ </feederoptions>
+
+ <spooler hostalias="node1" keepsuccess="true">
+ <abortondocumenterror>false</abortondocumenterror>
+ <maxpendingbytes>8000</maxpendingbytes>
+ <parsers>
+ <parser type="com.yahoo.vespaspooler.XMLFileParser"/>
+ <parser type="com.yahoo.vespaspooler.MusicFileParser"/>
+ <parser type="com.yahoo.vespaspooler.MusicParser">
+ <parameter key="route" value="default"/>
+ </parser>
+ </parsers>
+ </spooler>
+
+ <spooler hostalias="node1">
+ <abortondocumenterror>false</abortondocumenterror>
+ <maxpendingbytes>4000</maxpendingbytes>
+ <timeout>50</timeout>
+ <parsers>
+ <parser type="com.yahoo.vespaspooler.MusicParser">
+ <parameter key="route" value="othercluster"/>
+ </parser>
+ </parsers>
+ </spooler>
+
+ <spooler id="plan9">
+ <route>myroute</route>
+ <mbusport>14064</mbusport>
+
+ <parsers>
+ <parser type="com.yahoo.vespaspooler.MusicFileParser" />
+ </parsers>
+ </spooler>
+
+ </spoolers>
+ </clients>
+
+ <container version="1.0">
+ <search/>
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ </container>
+
+ <content version="1.0" id="music">
+ <redundancy>2</redundancy>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias="node1" distribution-key="0" />
+ </nodes>
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/hosts.xml b/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/searchdefinitions/music.sd b/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/searchdefinitions/music.sd
new file mode 100644
index 00000000000..136efeafaf6
--- /dev/null
+++ b/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/services.xml b/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/services.xml
new file mode 100644
index 00000000000..955256618a4
--- /dev/null
+++ b/config-model/src/test/cfg/clients/simpleconfig.v2.docprocv3/services.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ <slobroks>
+ <slobrok hostalias="node1" />
+ </slobroks>
+ </admin>
+
+ <clients version="2.0">
+ <spoolers>
+ <spooler hostalias="node1" maxfailuresize="100000" maxfatalfailuresize="1000000" threads="5">
+ <abortondocumenterror>false</abortondocumenterror>
+ <maxpendingbytes>8000</maxpendingbytes>
+ <tracelevel>7</tracelevel>
+ <parsers>
+ <parser type="com.yahoo.vespaspooler.XMLFileParser"/>
+ <parser type="com.yahoo.vespaspooler.MusicFileParser"/>
+ <parser type="com.yahoo.vespaspooler.MusicParser">
+ <parameter key="route" value="default"/>
+ <parameter key="foo" value="bar"/>
+ </parser>
+ </parsers>
+ </spooler>
+ </spoolers>
+ </clients>
+
+ <container version="1.0">
+
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+
+ <document-api/>
+
+ <document-processing>
+ <chain id="main">
+ <documentprocessor id="com.yahoo.docprocs.FoobarDocumentProcessor" />
+ </chain>
+ </document-processing>
+
+ <search/>
+
+ </container>
+
+ <content version="1.0" id="music">
+
+ <redundancy>2</redundancy>
+
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+
+ <nodes>
+ <node hostalias="node1" distribution-key="0" />
+ </nodes>
+
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/clients/simpleconfig.v2/searchdefinitions/.gitignore b/config-model/src/test/cfg/clients/simpleconfig.v2/searchdefinitions/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/clients/simpleconfig.v2/searchdefinitions/.gitignore
diff --git a/config-model/src/test/cfg/container/data/configserverinclude/hosted-vespa/hosted.xml b/config-model/src/test/cfg/container/data/configserverinclude/hosted-vespa/hosted.xml
new file mode 100644
index 00000000000..dbe3bb659e0
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/configserverinclude/hosted-vespa/hosted.xml
@@ -0,0 +1,10 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<jdisc>
+ <config name="cloud.config.elk">
+ <elasticsearch>
+ <item>
+ <host>foo</host>
+ </item>
+ </elasticsearch>
+ </config>
+</jdisc>
diff --git a/config-model/src/test/cfg/container/data/configserverinclude/services.xml b/config-model/src/test/cfg/container/data/configserverinclude/services.xml
new file mode 100644
index 00000000000..1cd2633c5e0
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/configserverinclude/services.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <jdisc id='configserver' version="1.0">
+ <http>
+ <server id="configserver" port="1234" />
+ </http>
+ </jdisc>
+</services>
diff --git a/config-model/src/test/cfg/container/data/containerinclude/docprocinclude1/foo/bar/docprocinclude1.xml b/config-model/src/test/cfg/container/data/containerinclude/docprocinclude1/foo/bar/docprocinclude1.xml
new file mode 100644
index 00000000000..80a49949581
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude/docprocinclude1/foo/bar/docprocinclude1.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<document-processing>
+ <chain id="docprocchain2">
+ <documentprocessor id="com.yahoo.DocumentProcessor2"/>
+ </chain>
+</document-processing>
diff --git a/config-model/src/test/cfg/container/data/containerinclude/hosts.xml b/config-model/src/test/cfg/container/data/containerinclude/hosts.xml
new file mode 100644
index 00000000000..3ab86a21aef
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/container/data/containerinclude/processinginclude1/processinginclude1.xml b/config-model/src/test/cfg/container/data/containerinclude/processinginclude1/processinginclude1.xml
new file mode 100644
index 00000000000..8ae46c0c9c7
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude/processinginclude1/processinginclude1.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<processing>
+ <chain id="processingchain2">
+ <processor id="com.yahoo.Processor2"/>
+ </chain>
+</processing>
diff --git a/config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch1.xml b/config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch1.xml
new file mode 100644
index 00000000000..9cdf3e767fe
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch1.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="searchchain2">
+ <searcher id="com.yahoo.Searcher2"/>
+ </chain>
+</search>
diff --git a/config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch2.xml b/config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch2.xml
new file mode 100644
index 00000000000..2cbe93a2cd1
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude/searchinclude1/contents/includedsearch2.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="searchchain3">
+ <searcher id="com.yahoo.Searcher3"/>
+ </chain>
+</search>
diff --git a/config-model/src/test/cfg/container/data/containerinclude/searchinclude2/includedsearch3.xml b/config-model/src/test/cfg/container/data/containerinclude/searchinclude2/includedsearch3.xml
new file mode 100644
index 00000000000..fbc0b5eb98b
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude/searchinclude2/includedsearch3.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="searchchain4">
+ <searcher id="com.yahoo.Searcher4"/>
+ </chain>
+</search>
diff --git a/config-model/src/test/cfg/container/data/containerinclude/services.xml b/config-model/src/test/cfg/container/data/containerinclude/services.xml
new file mode 100644
index 00000000000..172587ad8f8
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude/services.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <container version="1.0">
+ <search>
+ <include dir="searchinclude1"/>
+ <include dir="searchinclude2"/>
+ <chain id="searchchain1">
+ <searcher id="com.yahoo.Searcher1"/>
+ </chain>
+ </search>
+ <document-processing>
+ <include dir="docprocinclude1"/>
+ <chain id="docprocchain1">
+ <documentprocessor id="com.yahoo.DocumentProcessor1"/>
+ </chain>
+ </document-processing>
+ <processing>
+ <include dir="processinginclude1"/>
+ <chain id="processingchain1">
+ <processor id="com.yahoo.Processor1"/>
+ </chain>
+ </processing>
+
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ </container>
+
+</services>
diff --git a/config-model/src/test/cfg/container/data/containerinclude2/hosts.xml b/config-model/src/test/cfg/container/data/containerinclude2/hosts.xml
new file mode 100644
index 00000000000..3ab86a21aef
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude2/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/container/data/containerinclude2/services.xml b/config-model/src/test/cfg/container/data/containerinclude2/services.xml
new file mode 100644
index 00000000000..517e562d9d3
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude2/services.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <container version="1.0">
+ <document-processing>
+ <include dir="doesnotexist"/>
+ </document-processing>
+
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ </container>
+
+</services>
diff --git a/config-model/src/test/cfg/container/data/containerinclude3/hosts.xml b/config-model/src/test/cfg/container/data/containerinclude3/hosts.xml
new file mode 100644
index 00000000000..3ab86a21aef
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude3/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/container/data/containerinclude3/services.xml b/config-model/src/test/cfg/container/data/containerinclude3/services.xml
new file mode 100644
index 00000000000..40121832467
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude3/services.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <container version="1.0">
+ <document-processing>
+ <include dir="/bin"/>
+ </document-processing>
+
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ </container>
+
+</services>
diff --git a/config-model/src/test/cfg/container/data/containerinclude4/hosts.xml b/config-model/src/test/cfg/container/data/containerinclude4/hosts.xml
new file mode 100644
index 00000000000..3ab86a21aef
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude4/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/container/data/containerinclude4/services.xml b/config-model/src/test/cfg/container/data/containerinclude4/services.xml
new file mode 100644
index 00000000000..69b3dba40c0
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude4/services.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <container version="1.0">
+ <document-processing>
+ <include dir="hosts.xml"/>
+ </document-processing>
+
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ </container>
+
+</services>
diff --git a/config-model/src/test/cfg/container/data/containerinclude5/searchinclude/processing.xml b/config-model/src/test/cfg/container/data/containerinclude5/searchinclude/processing.xml
new file mode 100644
index 00000000000..98045c7bb0a
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude5/searchinclude/processing.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<processing>
+ <chain id="processingchain2">
+ <processor id="com.yahoo.Processor2"/>
+ </chain>
+</processing>
diff --git a/config-model/src/test/cfg/container/data/containerinclude5/services.xml b/config-model/src/test/cfg/container/data/containerinclude5/services.xml
new file mode 100644
index 00000000000..cf19fcd0b98
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude5/services.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <container version="1.0">
+ <search>
+ <include dir="searchinclude"/>
+ </search>
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ </container>
+
+</services>
diff --git a/config-model/src/test/cfg/container/data/containerinclude6/empty_dir/.gitignore b/config-model/src/test/cfg/container/data/containerinclude6/empty_dir/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude6/empty_dir/.gitignore
diff --git a/config-model/src/test/cfg/container/data/containerinclude6/services.xml b/config-model/src/test/cfg/container/data/containerinclude6/services.xml
new file mode 100644
index 00000000000..603e474c5f0
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/containerinclude6/services.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ </admin>
+
+ <container version="1.0">
+ <search>
+ <include dir="empty_dir"/>
+ </search>
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ </container>
+
+</services>
diff --git a/config-model/src/test/cfg/container/data/include_xml_error/dir1/default.xml b/config-model/src/test/cfg/container/data/include_xml_error/dir1/default.xml
new file mode 100644
index 00000000000..8e0472b4547
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/include_xml_error/dir1/default.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<search>
+ <chain id="default">
+ <zearcer id="com.yahoo.search.example.SimpleSearcher" bundle="mybundle"/>
+ </chain>
+</search>
diff --git a/config-model/src/test/cfg/container/data/include_xml_error/services.xml b/config-model/src/test/cfg/container/data/include_xml_error/services.xml
new file mode 100644
index 00000000000..a5a3cda10e4
--- /dev/null
+++ b/config-model/src/test/cfg/container/data/include_xml_error/services.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+
+ <jdisc version="1.0">
+ <search>
+ <include dir='dir1'/>
+ </search>
+
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+ </jdisc>
+
+</services>
diff --git a/config-model/src/test/cfg/routing/content_two_clusters/documentrouteselectorpolicy.cfg b/config-model/src/test/cfg/routing/content_two_clusters/documentrouteselectorpolicy.cfg
new file mode 100755
index 00000000000..657b05d63b7
--- /dev/null
+++ b/config-model/src/test/cfg/routing/content_two_clusters/documentrouteselectorpolicy.cfg
@@ -0,0 +1,6 @@
+route[0].name "content/mobile"
+route[0].selector "mobile or mobile_search"
+route[0].feed ""
+route[1].name "content/music"
+route[1].selector "music or music_search"
+route[1].feed ""
diff --git a/config-model/src/test/cfg/routing/content_two_clusters/hosts.xml b/config-model/src/test/cfg/routing/content_two_clusters/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/content_two_clusters/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/content_two_clusters/messagebus.cfg b/config-model/src/test/cfg/routing/content_two_clusters/messagebus.cfg
new file mode 100755
index 00000000000..3860bf71431
--- /dev/null
+++ b/config-model/src/test/cfg/routing/content_two_clusters/messagebus.cfg
@@ -0,0 +1,44 @@
+routingtable[0].protocol "document"
+routingtable[0].hop[0].name "docproc/cluster.mobile.indexing/chain.mobile.indexing"
+routingtable[0].hop[0].selector "[LoadBalancer:cluster=docproc/cluster.mobile.indexing;session=chain.mobile.indexing]"
+routingtable[0].hop[0].ignoreresult false
+routingtable[0].hop[1].name "docproc/cluster.music.indexing/chain.music.indexing"
+routingtable[0].hop[1].selector "[LoadBalancer:cluster=docproc/cluster.music.indexing;session=chain.music.indexing]"
+routingtable[0].hop[1].ignoreresult false
+routingtable[0].hop[2].name "indexing"
+routingtable[0].hop[2].selector "[DocumentRouteSelector]"
+routingtable[0].hop[2].recipient[0] "content/mobile"
+routingtable[0].hop[2].recipient[1] "content/music"
+routingtable[0].hop[2].ignoreresult false
+routingtable[0].route[0].name "content/mobile"
+routingtable[0].route[0].hop[0] "[MessageType:content/mobile]"
+routingtable[0].route[1].name "content/mobile-direct"
+routingtable[0].route[1].hop[0] "[Content:cluster=mobile]"
+routingtable[0].route[2].name "content/mobile-index"
+routingtable[0].route[2].hop[0] "docproc/cluster.mobile.indexing/chain.mobile.indexing"
+routingtable[0].route[2].hop[1] "[Content:cluster=mobile]"
+routingtable[0].route[3].name "content/music"
+routingtable[0].route[3].hop[0] "[MessageType:content/music]"
+routingtable[0].route[4].name "content/music-direct"
+routingtable[0].route[4].hop[0] "[Content:cluster=music]"
+routingtable[0].route[5].name "content/music-index"
+routingtable[0].route[5].hop[0] "docproc/cluster.music.indexing/chain.music.indexing"
+routingtable[0].route[5].hop[1] "[Content:cluster=music]"
+routingtable[0].route[6].name "default"
+routingtable[0].route[6].hop[0] "indexing"
+routingtable[0].route[7].name "mobile"
+routingtable[0].route[7].hop[0] "route:content/mobile"
+routingtable[0].route[8].name "mobile-direct"
+routingtable[0].route[8].hop[0] "route:content/mobile-direct"
+routingtable[0].route[9].name "mobile-index"
+routingtable[0].route[9].hop[0] "route:content/mobile-index"
+routingtable[0].route[10].name "music"
+routingtable[0].route[10].hop[0] "route:content/music"
+routingtable[0].route[11].name "music-direct"
+routingtable[0].route[11].hop[0] "route:content/music-direct"
+routingtable[0].route[12].name "music-index"
+routingtable[0].route[12].hop[0] "route:content/music-index"
+routingtable[0].route[13].name "storage/cluster.mobile"
+routingtable[0].route[13].hop[0] "route:content/mobile"
+routingtable[0].route[14].name "storage/cluster.music"
+routingtable[0].route[14].hop[0] "route:content/music"
diff --git a/config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/mobile.sd b/config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/mobile.sd
new file mode 100644
index 00000000000..ee52751f137
--- /dev/null
+++ b/config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/mobile.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search mobile {
+ document mobile {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ header
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ body
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/music.sd b/config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/music.sd
new file mode 100644
index 00000000000..136efeafaf6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/content_two_clusters/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/routing/content_two_clusters/services.xml b/config-model/src/test/cfg/routing/content_two_clusters/services.xml
new file mode 100644
index 00000000000..df9717c4980
--- /dev/null
+++ b/config-model/src/test/cfg/routing/content_two_clusters/services.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ <slobroks>
+ <slobrok hostalias="node1" />
+ </slobroks>
+ </admin>
+
+ <content version="1.0" id="music">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ <group name="mygroup">
+ <node hostalias="node1" distribution-key="0" />
+ </group>
+ </content>
+
+ <content version="1.0" id="mobile">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="mobile" mode="index"/>
+ </documents>
+ <group name="mygroup">
+ <node hostalias="node1" distribution-key="0" />
+ </group>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/routing/contentsimpleconfig/documentrouteselectorpolicy.cfg b/config-model/src/test/cfg/routing/contentsimpleconfig/documentrouteselectorpolicy.cfg
new file mode 100755
index 00000000000..7e7a546235c
--- /dev/null
+++ b/config-model/src/test/cfg/routing/contentsimpleconfig/documentrouteselectorpolicy.cfg
@@ -0,0 +1,3 @@
+route[0].name "content/music"
+route[0].selector "music or music_search"
+route[0].feed ""
diff --git a/config-model/src/test/cfg/routing/contentsimpleconfig/hosts.xml b/config-model/src/test/cfg/routing/contentsimpleconfig/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/contentsimpleconfig/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/contentsimpleconfig/messagebus.cfg b/config-model/src/test/cfg/routing/contentsimpleconfig/messagebus.cfg
new file mode 100755
index 00000000000..e1c87a4274d
--- /dev/null
+++ b/config-model/src/test/cfg/routing/contentsimpleconfig/messagebus.cfg
@@ -0,0 +1,25 @@
+routingtable[0].protocol "document"
+routingtable[0].hop[0].name "docproc/cluster.music.indexing/chain.music.indexing"
+routingtable[0].hop[0].selector "[LoadBalancer:cluster=docproc/cluster.music.indexing;session=chain.music.indexing]"
+routingtable[0].hop[0].ignoreresult false
+routingtable[0].hop[1].name "indexing"
+routingtable[0].hop[1].selector "[DocumentRouteSelector]"
+routingtable[0].hop[1].recipient[0] "content/music"
+routingtable[0].hop[1].ignoreresult false
+routingtable[0].route[0].name "content/music"
+routingtable[0].route[0].hop[0] "[MessageType:content/music]"
+routingtable[0].route[1].name "content/music-direct"
+routingtable[0].route[1].hop[0] "[Content:cluster=music]"
+routingtable[0].route[2].name "content/music-index"
+routingtable[0].route[2].hop[0] "docproc/cluster.music.indexing/chain.music.indexing"
+routingtable[0].route[2].hop[1] "[Content:cluster=music]"
+routingtable[0].route[3].name "default"
+routingtable[0].route[3].hop[0] "indexing"
+routingtable[0].route[4].name "music"
+routingtable[0].route[4].hop[0] "route:content/music"
+routingtable[0].route[5].name "music-direct"
+routingtable[0].route[5].hop[0] "route:content/music-direct"
+routingtable[0].route[6].name "music-index"
+routingtable[0].route[6].hop[0] "route:content/music-index"
+routingtable[0].route[7].name "storage/cluster.music"
+routingtable[0].route[7].hop[0] "route:content/music"
diff --git a/config-model/src/test/cfg/routing/contentsimpleconfig/searchdefinitions/music.sd b/config-model/src/test/cfg/routing/contentsimpleconfig/searchdefinitions/music.sd
new file mode 100644
index 00000000000..136efeafaf6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/contentsimpleconfig/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/routing/contentsimpleconfig/services.xml b/config-model/src/test/cfg/routing/contentsimpleconfig/services.xml
new file mode 100644
index 00000000000..b46e126444f
--- /dev/null
+++ b/config-model/src/test/cfg/routing/contentsimpleconfig/services.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ <slobroks>
+ <slobrok hostalias="node1" />
+ </slobroks>
+ </admin>
+
+ <content version="1.0" id="c">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ <group name="mygroup">
+ <node hostalias="node1" distribution-key="0" />
+ </group>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/routing/defaultconfig/documentrouteselectorpolicy.cfg b/config-model/src/test/cfg/routing/defaultconfig/documentrouteselectorpolicy.cfg
new file mode 100755
index 00000000000..8b137891791
--- /dev/null
+++ b/config-model/src/test/cfg/routing/defaultconfig/documentrouteselectorpolicy.cfg
@@ -0,0 +1 @@
+
diff --git a/config-model/src/test/cfg/routing/defaultconfig/hosts.xml b/config-model/src/test/cfg/routing/defaultconfig/hosts.xml
new file mode 100755
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/defaultconfig/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/defaultconfig/messagebus.cfg b/config-model/src/test/cfg/routing/defaultconfig/messagebus.cfg
new file mode 100755
index 00000000000..bddd1de480c
--- /dev/null
+++ b/config-model/src/test/cfg/routing/defaultconfig/messagebus.cfg
@@ -0,0 +1,6 @@
+routingtable[0].protocol "document"
+routingtable[0].route[0].name "aliasme"
+routingtable[0].route[0].hop[0] "docproc/cluster.music.indexing/*/chain.music.indexing"
+routingtable[0].route[0].hop[1] "search/search.music"
+routingtable[0].route[1].name "default"
+routingtable[0].route[1].hop[0] "route:aliasme"
diff --git a/config-model/src/test/cfg/routing/defaultconfig/services.xml b/config-model/src/test/cfg/routing/defaultconfig/services.xml
new file mode 100755
index 00000000000..03e6ac9ae9c
--- /dev/null
+++ b/config-model/src/test/cfg/routing/defaultconfig/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document" verify="false">
+ <route name="aliasme" hops="docproc/cluster.music.indexing/*/chain.music.indexing search/search.music" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/duplicatehop/errors.txt b/config-model/src/test/cfg/routing/duplicatehop/errors.txt
new file mode 100755
index 00000000000..ca0e52d6fb3
--- /dev/null
+++ b/config-model/src/test/cfg/routing/duplicatehop/errors.txt
@@ -0,0 +1 @@
+Hop 'foo' in routing table 'document' is defined 2 times.
diff --git a/config-model/src/test/cfg/routing/duplicatehop/hosts.xml b/config-model/src/test/cfg/routing/duplicatehop/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/duplicatehop/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/duplicatehop/services.xml b/config-model/src/test/cfg/routing/duplicatehop/services.xml
new file mode 100755
index 00000000000..e69f2bf5798
--- /dev/null
+++ b/config-model/src/test/cfg/routing/duplicatehop/services.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="bar/baz" />
+ <hop name="foo" selector="baz/cox" />
+ </routingtable>
+ <services protocol="document">
+ <service name="bar/baz" />
+ <service name="baz/cox" />
+ </services>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/duplicateroute/errors.txt b/config-model/src/test/cfg/routing/duplicateroute/errors.txt
new file mode 100755
index 00000000000..2fd943e5a27
--- /dev/null
+++ b/config-model/src/test/cfg/routing/duplicateroute/errors.txt
@@ -0,0 +1 @@
+Route 'foo' in routing table 'document' is defined 2 times.
diff --git a/config-model/src/test/cfg/routing/duplicateroute/hosts.xml b/config-model/src/test/cfg/routing/duplicateroute/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/duplicateroute/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/duplicateroute/services.xml b/config-model/src/test/cfg/routing/duplicateroute/services.xml
new file mode 100755
index 00000000000..33fc9a12717
--- /dev/null
+++ b/config-model/src/test/cfg/routing/duplicateroute/services.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <route name="foo" hops="bar" />
+ <route name="foo" hops="baz" />
+ </routingtable>
+ <services protocol="document">
+ <service name="bar" />
+ <service name="baz" />
+ </services>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/emptyhop/errors.txt b/config-model/src/test/cfg/routing/emptyhop/errors.txt
new file mode 100755
index 00000000000..85317424e5b
--- /dev/null
+++ b/config-model/src/test/cfg/routing/emptyhop/errors.txt
@@ -0,0 +1 @@
+For hop 'foo' in routing table 'document'; Failed to parse empty string.
diff --git a/config-model/src/test/cfg/routing/emptyhop/hosts.xml b/config-model/src/test/cfg/routing/emptyhop/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/emptyhop/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/emptyhop/services.xml b/config-model/src/test/cfg/routing/emptyhop/services.xml
new file mode 100644
index 00000000000..1cb8136219b
--- /dev/null
+++ b/config-model/src/test/cfg/routing/emptyhop/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/emptyroute/errors.txt b/config-model/src/test/cfg/routing/emptyroute/errors.txt
new file mode 100755
index 00000000000..66f38f6cabe
--- /dev/null
+++ b/config-model/src/test/cfg/routing/emptyroute/errors.txt
@@ -0,0 +1 @@
+Route 'foo' in routing table 'document' has no hops.
diff --git a/config-model/src/test/cfg/routing/emptyroute/hosts.xml b/config-model/src/test/cfg/routing/emptyroute/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/emptyroute/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/emptyroute/services.xml b/config-model/src/test/cfg/routing/emptyroute/services.xml
new file mode 100644
index 00000000000..4112fdf2062
--- /dev/null
+++ b/config-model/src/test/cfg/routing/emptyroute/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <route name="foo" hops="" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/hopconfig/documentrouteselectorpolicy.cfg b/config-model/src/test/cfg/routing/hopconfig/documentrouteselectorpolicy.cfg
new file mode 100755
index 00000000000..8b137891791
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hopconfig/documentrouteselectorpolicy.cfg
@@ -0,0 +1 @@
+
diff --git a/config-model/src/test/cfg/routing/hopconfig/hosts.xml b/config-model/src/test/cfg/routing/hopconfig/hosts.xml
new file mode 100755
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hopconfig/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/hopconfig/messagebus.cfg b/config-model/src/test/cfg/routing/hopconfig/messagebus.cfg
new file mode 100755
index 00000000000..f15f89f3a58
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hopconfig/messagebus.cfg
@@ -0,0 +1,15 @@
+routingtable[0].protocol "document"
+routingtable[0].hop[0].name "backdoor"
+routingtable[0].hop[0].selector "search/cluster.music/[SearchRow:1]/[SearchColumn:1]/feed-destination"
+routingtable[0].hop[0].recipient[0] "search/cluster.music/r0/c0/feed-destination"
+routingtable[0].hop[0].recipient[1] "search/cluster.music/r0/c1/feed-destination"
+routingtable[0].hop[0].recipient[2] "search/cluster.music/r1/c0/feed-destination"
+routingtable[0].hop[0].recipient[3] "search/cluster.music/r1/c1/feed-destination"
+routingtable[0].hop[0].recipient[4] "search/cluster.music/r0/c0/feed-destination"
+routingtable[0].hop[0].recipient[5] "search/cluster.music/r0/c1/feed-destination"
+routingtable[0].hop[0].recipient[6] "search/cluster.music/r1/c0/feed-destination"
+routingtable[0].hop[0].recipient[7] "search/cluster.music/r1/c1/feed-destination"
+routingtable[0].hop[0].ignoreresult false
+routingtable[0].hop[1].name "foo"
+routingtable[0].hop[1].selector "bar"
+routingtable[0].hop[1].ignoreresult true
diff --git a/config-model/src/test/cfg/routing/hopconfig/services.xml b/config-model/src/test/cfg/routing/hopconfig/services.xml
new file mode 100755
index 00000000000..87b18f4ceda
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hopconfig/services.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document" verify="false">
+ <hop name="backdoor" selector=
+ "search/cluster.music/[SearchRow:1]/[SearchColumn:1]/feed-destination">
+ <recipient session="search/cluster.music/r0/c0/feed-destination" />
+ <recipient session="search/cluster.music/r0/c1/feed-destination" />
+ <recipient session="search/cluster.music/r1/c0/feed-destination" />
+ <recipient session="search/cluster.music/r1/c1/feed-destination" />
+ <recipient session="search/cluster.music/r0/c0/feed-destination" />
+ <recipient session="search/cluster.music/r0/c1/feed-destination" />
+ <recipient session="search/cluster.music/r1/c0/feed-destination" />
+ <recipient session="search/cluster.music/r1/c1/feed-destination" />
+ </hop>
+ <hop name="foo" selector="bar" ignore-result="true" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/hoperror/errors.txt b/config-model/src/test/cfg/routing/hoperror/errors.txt
new file mode 100755
index 00000000000..fd0ec8b453c
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperror/errors.txt
@@ -0,0 +1 @@
+For hop 'foo' in routing table 'document'; Failed to completely parse 'bar/baz cox'.
diff --git a/config-model/src/test/cfg/routing/hoperror/hosts.xml b/config-model/src/test/cfg/routing/hoperror/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperror/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/hoperror/services.xml b/config-model/src/test/cfg/routing/hoperror/services.xml
new file mode 100644
index 00000000000..f31a60d03d7
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperror/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="bar/baz cox" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/hoperrorinrecipient/errors.txt b/config-model/src/test/cfg/routing/hoperrorinrecipient/errors.txt
new file mode 100755
index 00000000000..339a569bf6e
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperrorinrecipient/errors.txt
@@ -0,0 +1 @@
+For recipient '[baz]]' in hop 'foo' in routing table 'document'; Unexpected token ']' in '[baz]]'
diff --git a/config-model/src/test/cfg/routing/hoperrorinrecipient/hosts.xml b/config-model/src/test/cfg/routing/hoperrorinrecipient/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperrorinrecipient/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/hoperrorinrecipient/services.xml b/config-model/src/test/cfg/routing/hoperrorinrecipient/services.xml
new file mode 100644
index 00000000000..e2a0a5827be
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperrorinrecipient/services.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="[bar]">
+ <recipient session="[baz]]" />
+ </hop>
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/hoperrorinroute/errors.txt b/config-model/src/test/cfg/routing/hoperrorinroute/errors.txt
new file mode 100755
index 00000000000..4640a965903
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperrorinroute/errors.txt
@@ -0,0 +1 @@
+For hop 1 in route 'foo' in routing table 'document'; Unexpected token ']' in '[bar]] baz'
diff --git a/config-model/src/test/cfg/routing/hoperrorinroute/hosts.xml b/config-model/src/test/cfg/routing/hoperrorinroute/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperrorinroute/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/hoperrorinroute/services.xml b/config-model/src/test/cfg/routing/hoperrorinroute/services.xml
new file mode 100644
index 00000000000..61a334440fc
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hoperrorinroute/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <route name="foo" hops="[bar]] baz" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/hopnotfound/errors.txt b/config-model/src/test/cfg/routing/hopnotfound/errors.txt
new file mode 100755
index 00000000000..8b999742157
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hopnotfound/errors.txt
@@ -0,0 +1 @@
+Hop 1 in route 'foo' in routing table 'document' references 'bar' which is neither a service, a route nor another hop.
diff --git a/config-model/src/test/cfg/routing/hopnotfound/hosts.xml b/config-model/src/test/cfg/routing/hopnotfound/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hopnotfound/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/hopnotfound/services.xml b/config-model/src/test/cfg/routing/hopnotfound/services.xml
new file mode 100644
index 00000000000..ed859c34a8f
--- /dev/null
+++ b/config-model/src/test/cfg/routing/hopnotfound/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <route name="foo" hops="bar" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/invalidstoragepolicy/errors.txt b/config-model/src/test/cfg/routing/invalidstoragepolicy/errors.txt
new file mode 100644
index 00000000000..797dee3fd88
--- /dev/null
+++ b/config-model/src/test/cfg/routing/invalidstoragepolicy/errors.txt
@@ -0,0 +1 @@
+Can't use storage policy with only slobrok in 5.0 and hierarchical grouping \ No newline at end of file
diff --git a/config-model/src/test/cfg/routing/invalidstoragepolicy/hosts.xml b/config-model/src/test/cfg/routing/invalidstoragepolicy/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/invalidstoragepolicy/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/invalidstoragepolicy/services.xml b/config-model/src/test/cfg/routing/invalidstoragepolicy/services.xml
new file mode 100644
index 00000000000..806e1cde02c
--- /dev/null
+++ b/config-model/src/test/cfg/routing/invalidstoragepolicy/services.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <route name="foo" hops="[Storage:cluster=storage;slobroks=donald.duck.example.com:14020"/>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/mismatchedrecipient/errors.txt b/config-model/src/test/cfg/routing/mismatchedrecipient/errors.txt
new file mode 100755
index 00000000000..7be10cc0c6f
--- /dev/null
+++ b/config-model/src/test/cfg/routing/mismatchedrecipient/errors.txt
@@ -0,0 +1 @@
+Selector 'bar/[baz]/cox' does not match recipient 'cox/0/bar' in hop 'foo' in routing table 'document'.
diff --git a/config-model/src/test/cfg/routing/mismatchedrecipient/hosts.xml b/config-model/src/test/cfg/routing/mismatchedrecipient/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/mismatchedrecipient/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/mismatchedrecipient/services.xml b/config-model/src/test/cfg/routing/mismatchedrecipient/services.xml
new file mode 100644
index 00000000000..32a4c99066e
--- /dev/null
+++ b/config-model/src/test/cfg/routing/mismatchedrecipient/services.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="bar/[baz]/cox">
+ <recipient session="cox/0/bar" />
+ </hop>
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/replacehop/documentrouteselectorpolicy.cfg b/config-model/src/test/cfg/routing/replacehop/documentrouteselectorpolicy.cfg
new file mode 100755
index 00000000000..375e89f7419
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replacehop/documentrouteselectorpolicy.cfg
@@ -0,0 +1,3 @@
+route[0].name "music"
+route[0].selector "(music)"
+route[0].feed ""
diff --git a/config-model/src/test/cfg/routing/replacehop/hosts.xml b/config-model/src/test/cfg/routing/replacehop/hosts.xml
new file mode 100755
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replacehop/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/replacehop/messagebus.cfg b/config-model/src/test/cfg/routing/replacehop/messagebus.cfg
new file mode 100755
index 00000000000..ad8fb260824
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replacehop/messagebus.cfg
@@ -0,0 +1,22 @@
+routingtable[0].protocol "document"
+routingtable[0].hop[0].name "docproc/cluster.music.indexing/chain.indexing"
+routingtable[0].hop[0].selector "[LoadBalancer:cluster=docproc/cluster.music.indexing;session=chain.indexing]"
+routingtable[0].hop[0].ignoreresult false
+routingtable[0].hop[1].name "indexing"
+routingtable[0].hop[1].selector "[DocumentRouteSelector]"
+routingtable[0].hop[1].recipient[0] "music"
+routingtable[0].hop[1].ignoreresult false
+routingtable[0].hop[2].name "search/cluster.music"
+routingtable[0].hop[2].selector "foo"
+routingtable[0].hop[2].ignoreresult false
+routingtable[0].route[0].name "default"
+routingtable[0].route[0].hop[0] "indexing"
+routingtable[0].route[1].name "music"
+routingtable[0].route[1].hop[0] "[MessageType:music]"
+routingtable[0].route[2].name "music-direct"
+routingtable[0].route[2].hop[0] "[Content:cluster=music]"
+routingtable[0].route[3].name "music-index"
+routingtable[0].route[3].hop[0] "docproc/cluster.music.indexing/chain.indexing"
+routingtable[0].route[3].hop[1] "[Content:cluster=music]"
+routingtable[0].route[4].name "storage/cluster.music"
+routingtable[0].route[4].hop[0] "route:music"
diff --git a/config-model/src/test/cfg/routing/replacehop/searchdefinitions/music.sd b/config-model/src/test/cfg/routing/replacehop/searchdefinitions/music.sd
new file mode 100755
index 00000000000..6c6c3e15783
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replacehop/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/routing/replacehop/services.xml b/config-model/src/test/cfg/routing/replacehop/services.xml
new file mode 100755
index 00000000000..1baaf6a9af0
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replacehop/services.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ <slobroks>
+ <slobrok hostalias="node1" />
+ </slobroks>
+ </admin>
+
+ <content id="music" version="1.0">
+ <redundancy>1</redundancy>
+ <nodes>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ </content>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="search/cluster.music" selector="foo" />
+ </routingtable>
+ <services protocol="document">
+ <service name="foo" />
+ </services>
+ </routing>
+
+</services>
diff --git a/config-model/src/test/cfg/routing/replaceroute/documentrouteselectorpolicy.cfg b/config-model/src/test/cfg/routing/replaceroute/documentrouteselectorpolicy.cfg
new file mode 100755
index 00000000000..375e89f7419
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replaceroute/documentrouteselectorpolicy.cfg
@@ -0,0 +1,3 @@
+route[0].name "music"
+route[0].selector "(music)"
+route[0].feed ""
diff --git a/config-model/src/test/cfg/routing/replaceroute/hosts.xml b/config-model/src/test/cfg/routing/replaceroute/hosts.xml
new file mode 100755
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replaceroute/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/replaceroute/messagebus.cfg b/config-model/src/test/cfg/routing/replaceroute/messagebus.cfg
new file mode 100755
index 00000000000..e9389a2a6d9
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replaceroute/messagebus.cfg
@@ -0,0 +1,19 @@
+routingtable[0].protocol "document"
+routingtable[0].hop[0].name "docproc/cluster.music.indexing/chain.indexing"
+routingtable[0].hop[0].selector "[LoadBalancer:cluster=docproc/cluster.music.indexing;session=chain.indexing]"
+routingtable[0].hop[0].ignoreresult false
+routingtable[0].hop[1].name "indexing"
+routingtable[0].hop[1].selector "[DocumentRouteSelector]"
+routingtable[0].hop[1].recipient[0] "music"
+routingtable[0].hop[1].ignoreresult false
+routingtable[0].route[0].name "default"
+routingtable[0].route[0].hop[0] "foo"
+routingtable[0].route[1].name "music"
+routingtable[0].route[1].hop[0] "[MessageType:music]"
+routingtable[0].route[2].name "music-direct"
+routingtable[0].route[2].hop[0] "[Content:cluster=music]"
+routingtable[0].route[3].name "music-index"
+routingtable[0].route[3].hop[0] "docproc/cluster.music.indexing/chain.indexing"
+routingtable[0].route[3].hop[1] "[Content:cluster=music]"
+routingtable[0].route[4].name "storage/cluster.music"
+routingtable[0].route[4].hop[0] "route:music"
diff --git a/config-model/src/test/cfg/routing/replaceroute/searchdefinitions/music.sd b/config-model/src/test/cfg/routing/replaceroute/searchdefinitions/music.sd
new file mode 100755
index 00000000000..6c6c3e15783
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replaceroute/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/routing/replaceroute/services.xml b/config-model/src/test/cfg/routing/replaceroute/services.xml
new file mode 100755
index 00000000000..7c39e37e002
--- /dev/null
+++ b/config-model/src/test/cfg/routing/replaceroute/services.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ <slobroks>
+ <slobrok hostalias="node1" />
+ </slobroks>
+ </admin>
+ <content version="1.0" id="music">
+ <redundancy>1</redundancy>
+ <nodes>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ </content>
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <route name="default" hops="foo" />
+ </routingtable>
+ <services protocol="document">
+ <service name="foo" />
+ </services>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/routeconfig/documentrouteselectorpolicy.cfg b/config-model/src/test/cfg/routing/routeconfig/documentrouteselectorpolicy.cfg
new file mode 100755
index 00000000000..8b137891791
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routeconfig/documentrouteselectorpolicy.cfg
@@ -0,0 +1 @@
+
diff --git a/config-model/src/test/cfg/routing/routeconfig/hosts.xml b/config-model/src/test/cfg/routing/routeconfig/hosts.xml
new file mode 100755
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routeconfig/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/routeconfig/messagebus.cfg b/config-model/src/test/cfg/routing/routeconfig/messagebus.cfg
new file mode 100755
index 00000000000..556cbef7556
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routeconfig/messagebus.cfg
@@ -0,0 +1,7 @@
+routingtable[0].protocol "document"
+routingtable[0].route[0].name "backdoor"
+routingtable[0].route[0].hop[0] "docproc/cluster.music.indexing/*/chain.music.indexing"
+routingtable[0].route[0].hop[1] "backdoor"
+routingtable[0].route[1].name "default"
+routingtable[0].route[1].hop[0] "docproc/cluster.blacklist/*/chain.blacklist"
+routingtable[0].route[1].hop[1] "indexing"
diff --git a/config-model/src/test/cfg/routing/routeconfig/services.xml b/config-model/src/test/cfg/routing/routeconfig/services.xml
new file mode 100755
index 00000000000..bf004ab0cf9
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routeconfig/services.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document" verify="false">
+ <route name="default" hops="docproc/cluster.blacklist/*/chain.blacklist indexing" />
+ <route name="backdoor" hops="docproc/cluster.music.indexing/*/chain.music.indexing backdoor" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/routenotfound/errors.txt b/config-model/src/test/cfg/routing/routenotfound/errors.txt
new file mode 100755
index 00000000000..a2cd1a1a6f1
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routenotfound/errors.txt
@@ -0,0 +1 @@
+Hop 'foo' in routing table 'document' references route 'bar' which does not exist.
diff --git a/config-model/src/test/cfg/routing/routenotfound/hosts.xml b/config-model/src/test/cfg/routing/routenotfound/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routenotfound/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/routenotfound/services.xml b/config-model/src/test/cfg/routing/routenotfound/services.xml
new file mode 100644
index 00000000000..72267cf394d
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routenotfound/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="route:bar" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/routenotfoundinroute/errors.txt b/config-model/src/test/cfg/routing/routenotfoundinroute/errors.txt
new file mode 100755
index 00000000000..536e82a09b1
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routenotfoundinroute/errors.txt
@@ -0,0 +1 @@
+Hop 1 in route 'foo' in routing table 'document' references route 'bar' which does not exist.
diff --git a/config-model/src/test/cfg/routing/routenotfoundinroute/hosts.xml b/config-model/src/test/cfg/routing/routenotfoundinroute/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routenotfoundinroute/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/routenotfoundinroute/services.xml b/config-model/src/test/cfg/routing/routenotfoundinroute/services.xml
new file mode 100644
index 00000000000..0083d36933f
--- /dev/null
+++ b/config-model/src/test/cfg/routing/routenotfoundinroute/services.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <route name="foo" hops="route:bar" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/servicenotfound/errors.txt b/config-model/src/test/cfg/routing/servicenotfound/errors.txt
new file mode 100755
index 00000000000..430cd317a90
--- /dev/null
+++ b/config-model/src/test/cfg/routing/servicenotfound/errors.txt
@@ -0,0 +1 @@
+Hop 'foo' in routing table 'document' references 'bar/baz' which is neither a service, a route nor another hop.
diff --git a/config-model/src/test/cfg/routing/servicenotfound/hosts.xml b/config-model/src/test/cfg/routing/servicenotfound/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/servicenotfound/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/servicenotfound/services.xml b/config-model/src/test/cfg/routing/servicenotfound/services.xml
new file mode 100644
index 00000000000..9f3822eb89d
--- /dev/null
+++ b/config-model/src/test/cfg/routing/servicenotfound/services.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="bar/baz" />
+ </routingtable>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/routing/unexpectedrecipient/errors.txt b/config-model/src/test/cfg/routing/unexpectedrecipient/errors.txt
new file mode 100755
index 00000000000..3972c29adcf
--- /dev/null
+++ b/config-model/src/test/cfg/routing/unexpectedrecipient/errors.txt
@@ -0,0 +1 @@
+Hop 'foo' in routing table 'document' has recipients but no policy directive.
diff --git a/config-model/src/test/cfg/routing/unexpectedrecipient/hosts.xml b/config-model/src/test/cfg/routing/unexpectedrecipient/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/routing/unexpectedrecipient/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/routing/unexpectedrecipient/services.xml b/config-model/src/test/cfg/routing/unexpectedrecipient/services.xml
new file mode 100644
index 00000000000..aa0ac5c896a
--- /dev/null
+++ b/config-model/src/test/cfg/routing/unexpectedrecipient/services.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1" />
+ <logserver hostalias="node1" />
+ </admin>
+
+ <routing version="1.0">
+ <routingtable protocol="document">
+ <hop name="foo" selector="bar">
+ <recipient session="baz" />
+ </hop>
+ </routingtable>
+ <services protocol="document">
+ <service name="bar" />
+ <service name="baz" />
+ </services>
+ </routing>
+</services>
diff --git a/config-model/src/test/cfg/search/compare/complex/hosts/dev-mathiasm/sentinel/.gitignore b/config-model/src/test/cfg/search/compare/complex/hosts/dev-mathiasm/sentinel/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/hosts/dev-mathiasm/sentinel/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/hosts/zarya/sentinel/.gitignore b/config-model/src/test/cfg/search/compare/complex/hosts/zarya/sentinel/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/hosts/zarya/sentinel/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..c5b1dfef610
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19125
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r1/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r1/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..7aed9bdc244
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c0/r1/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19131
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..49e5f59b9be
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19137
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r1/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r1/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..8d5d4fdde7f
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/c1/r1/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19143
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r0/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r1/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r1/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c0/r1/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r0/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r1/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r1/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/g0/c1/r1/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..be7d8e44c16
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19110
+slobrok.name "search/cluster.music/rtx/0/clustercontroller"
+slobrok.config search/cluster.music/rtx
+servicemonitor.autodisable false
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/1/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/1/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..f39dc6adc03
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/rtx/1/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19112
+slobrok.name "search/cluster.music/rtx/1/clustercontroller"
+slobrok.config search/cluster.music/rtx
+servicemonitor.autodisable false
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg
new file mode 100644
index 00000000000..e9368ca2662
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg
@@ -0,0 +1,23 @@
+fnetlockwait 20.0
+defaultslowdocsumlimitbias 1.0
+defaultslowdocsumlimitfactor 2.0
+defaultslowquerylimitbias 1.0
+defaultslowquerylimitfactor 2.0
+frtport 19114
+healthport 19116
+indexswitchminsearchgrace 0.0
+indexswitchmindocsumgrace 0.0
+indexswitchmaxsearchgrace 0.0
+indexswitchmaxdocsumgrace 64.0
+maxsocksilent 50.0
+maxthreads 1000
+transportthreads 1
+partition 0
+ptport 19115
+transport ""
+transportnodelay true
+transportdirectwrite false
+packetcompresslimit 1024
+packetcompresslevel 3
+packetcompresstype LZ4
+dispatchlevel 0
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg
new file mode 100644
index 00000000000..bde0b169ef4
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg
@@ -0,0 +1,66 @@
+dataset[0].id 0
+dataset[0].refcost 1
+dataset[0].partbits 8
+dataset[0].rowbits 1
+dataset[0].numparts 2
+dataset[0].firstpart 0
+dataset[0].minpartitions 0
+dataset[0].mpp 2
+dataset[0].maxnodesdownperfixedrow 0
+dataset[0].useroundrobinforfixedrow true
+dataset[0].allowsearchonwarmupnodes true
+dataset[0].beforewarmupdelay 600.0
+dataset[0].engine[0].name_and_port "tcp/zarya:19121"
+dataset[0].engine[0].partid 0
+dataset[0].engine[0].rowid 0
+dataset[0].engine[0].subdatasetid 0
+dataset[0].engine[0].refcost 1
+dataset[0].engine[0].overridepartids true
+dataset[0].engine[1].name_and_port "tcp/zarya:19127"
+dataset[0].engine[1].partid 0
+dataset[0].engine[1].rowid 1
+dataset[0].engine[1].subdatasetid 0
+dataset[0].engine[1].refcost 1
+dataset[0].engine[1].overridepartids true
+dataset[0].engine[2].name_and_port "tcp/zarya:19133"
+dataset[0].engine[2].partid 1
+dataset[0].engine[2].rowid 0
+dataset[0].engine[2].subdatasetid 0
+dataset[0].engine[2].refcost 1
+dataset[0].engine[2].overridepartids true
+dataset[0].engine[3].name_and_port "tcp/zarya:19139"
+dataset[0].engine[3].partid 1
+dataset[0].engine[3].rowid 1
+dataset[0].engine[3].subdatasetid 0
+dataset[0].engine[3].refcost 1
+dataset[0].engine[3].overridepartids true
+dataset[0].maxhitspernode 2147483647
+dataset[0].estparts 0
+dataset[0].estpartcutoff 0
+dataset[0].minactive 500
+dataset[0].maxactive 500
+dataset[0].cutoffactive 1000
+dataset[0].minestactive 500
+dataset[0].maxestactive 1000
+dataset[0].cutoffestactive 1000
+dataset[0].queuedrainrate 400.0
+dataset[0].queuedrainmax 40.0
+dataset[0].slowquerylimitfactor 0.0
+dataset[0].slowquerylimitbias 100.0
+dataset[0].slowdocsumlimitfactor 0.0
+dataset[0].slowdocsumlimitbias 100.0
+dataset[0].monitorinterval 1.0
+dataset[0].higher_coverage_maxsearchwait 1.0
+dataset[0].higher_coverage_minsearchwait 0.0
+dataset[0].higher_coverage_basesearchwait 0.1
+dataset[0].minimal_searchcoverage 100.0
+dataset[0].higher_coverage_maxdocsumwait 0.3
+dataset[0].higher_coverage_mindocsumwait 0.1
+dataset[0].higher_coverage_basedocsumwait 0.1
+dataset[0].minimal_docsumcoverage 100.0
+dataset[0].querydistribution AUTOMATIC
+dataset[0].min_group_coverage 100.0
+dataset[0].min_activedocs_coverage 97.0
+dataset[0].latency_decay_rate 10000.0
+dataset[0].querydistributionsamplesize 10000
+dataset[0].querydistributionconfidenceinterval 99.9
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/fdispatchrc.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/fdispatchrc.MODEL.cfg
new file mode 100644
index 00000000000..aa48d5fec79
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/fdispatchrc.MODEL.cfg
@@ -0,0 +1,23 @@
+fnetlockwait 20.0
+defaultslowdocsumlimitbias 1.0
+defaultslowdocsumlimitfactor 2.0
+defaultslowquerylimitbias 1.0
+defaultslowquerylimitfactor 2.0
+frtport 19117
+healthport 19119
+indexswitchminsearchgrace 0.0
+indexswitchmindocsumgrace 0.0
+indexswitchmaxsearchgrace 0.0
+indexswitchmaxdocsumgrace 64.0
+maxsocksilent 50.0
+maxthreads 1000
+transportthreads 1
+partition 0
+ptport 19118
+transport ""
+transportnodelay true
+transportdirectwrite false
+packetcompresslimit 1024
+packetcompresslevel 3
+packetcompresstype LZ4
+dispatchlevel 0
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/partitions.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/partitions.MODEL.cfg
new file mode 100644
index 00000000000..bde0b169ef4
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.music/tlds/tld.1/partitions.MODEL.cfg
@@ -0,0 +1,66 @@
+dataset[0].id 0
+dataset[0].refcost 1
+dataset[0].partbits 8
+dataset[0].rowbits 1
+dataset[0].numparts 2
+dataset[0].firstpart 0
+dataset[0].minpartitions 0
+dataset[0].mpp 2
+dataset[0].maxnodesdownperfixedrow 0
+dataset[0].useroundrobinforfixedrow true
+dataset[0].allowsearchonwarmupnodes true
+dataset[0].beforewarmupdelay 600.0
+dataset[0].engine[0].name_and_port "tcp/zarya:19121"
+dataset[0].engine[0].partid 0
+dataset[0].engine[0].rowid 0
+dataset[0].engine[0].subdatasetid 0
+dataset[0].engine[0].refcost 1
+dataset[0].engine[0].overridepartids true
+dataset[0].engine[1].name_and_port "tcp/zarya:19127"
+dataset[0].engine[1].partid 0
+dataset[0].engine[1].rowid 1
+dataset[0].engine[1].subdatasetid 0
+dataset[0].engine[1].refcost 1
+dataset[0].engine[1].overridepartids true
+dataset[0].engine[2].name_and_port "tcp/zarya:19133"
+dataset[0].engine[2].partid 1
+dataset[0].engine[2].rowid 0
+dataset[0].engine[2].subdatasetid 0
+dataset[0].engine[2].refcost 1
+dataset[0].engine[2].overridepartids true
+dataset[0].engine[3].name_and_port "tcp/zarya:19139"
+dataset[0].engine[3].partid 1
+dataset[0].engine[3].rowid 1
+dataset[0].engine[3].subdatasetid 0
+dataset[0].engine[3].refcost 1
+dataset[0].engine[3].overridepartids true
+dataset[0].maxhitspernode 2147483647
+dataset[0].estparts 0
+dataset[0].estpartcutoff 0
+dataset[0].minactive 500
+dataset[0].maxactive 500
+dataset[0].cutoffactive 1000
+dataset[0].minestactive 500
+dataset[0].maxestactive 1000
+dataset[0].cutoffestactive 1000
+dataset[0].queuedrainrate 400.0
+dataset[0].queuedrainmax 40.0
+dataset[0].slowquerylimitfactor 0.0
+dataset[0].slowquerylimitbias 100.0
+dataset[0].slowdocsumlimitfactor 0.0
+dataset[0].slowdocsumlimitbias 100.0
+dataset[0].monitorinterval 1.0
+dataset[0].higher_coverage_maxsearchwait 1.0
+dataset[0].higher_coverage_minsearchwait 0.0
+dataset[0].higher_coverage_basesearchwait 0.1
+dataset[0].minimal_searchcoverage 100.0
+dataset[0].higher_coverage_maxdocsumwait 0.3
+dataset[0].higher_coverage_mindocsumwait 0.1
+dataset[0].higher_coverage_basedocsumwait 0.1
+dataset[0].minimal_docsumcoverage 100.0
+dataset[0].querydistribution AUTOMATIC
+dataset[0].min_group_coverage 100.0
+dataset[0].min_activedocs_coverage 97.0
+dataset[0].latency_decay_rate 10000.0
+dataset[0].querydistributionsamplesize 10000
+dataset[0].querydistributionconfidenceinterval 99.9
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..5c9f46bf8ce
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19156
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r1/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r1/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..69f91eab48c
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c0/r1/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19105
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..eebb1cb6b40
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19111
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r1/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r1/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..82e9aafc5a8
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/c1/r1/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19162
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r0/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r1/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r1/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c0/r1/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r0/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r1/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r1/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/g0/c1/r1/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx-rtlogic.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx-rtlogic.MODEL.cfg
new file mode 100644
index 00000000000..9c332f32703
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx-rtlogic.MODEL.cfg
@@ -0,0 +1,19 @@
+parts 2
+minnodesperpart 1
+maxbadparts 0
+cluster.rtc[0].spec "tcp/zarya:19151"
+cluster.rtc[0].part 0
+cluster.rtc[0].row 0
+cluster.rtc[0].feedservice "search/cluster.rt/c0/r0/feed-destination"
+cluster.rtc[1].spec "tcp/dev-mathiasm:19100"
+cluster.rtc[1].part 0
+cluster.rtc[1].row 1
+cluster.rtc[1].feedservice "search/cluster.rt/c0/r1/feed-destination"
+cluster.rtc[2].spec "tcp/dev-mathiasm:19106"
+cluster.rtc[2].part 1
+cluster.rtc[2].row 0
+cluster.rtc[2].feedservice "search/cluster.rt/c1/r0/feed-destination"
+cluster.rtc[3].spec "tcp/zarya:19157"
+cluster.rtc[3].part 1
+cluster.rtc[3].row 1
+cluster.rtc[3].feedservice "search/cluster.rt/c1/r1/feed-destination"
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..aa557a9ae04
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/0/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19144
+slobrok.name "search/cluster.rt/rtx/0/clustercontroller"
+slobrok.config search/cluster.rt/rtx
+servicemonitor.autodisable true
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx-rtlogic.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx-rtlogic.MODEL.cfg
new file mode 100644
index 00000000000..9c332f32703
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx-rtlogic.MODEL.cfg
@@ -0,0 +1,19 @@
+parts 2
+minnodesperpart 1
+maxbadparts 0
+cluster.rtc[0].spec "tcp/zarya:19151"
+cluster.rtc[0].part 0
+cluster.rtc[0].row 0
+cluster.rtc[0].feedservice "search/cluster.rt/c0/r0/feed-destination"
+cluster.rtc[1].spec "tcp/dev-mathiasm:19100"
+cluster.rtc[1].part 0
+cluster.rtc[1].row 1
+cluster.rtc[1].feedservice "search/cluster.rt/c0/r1/feed-destination"
+cluster.rtc[2].spec "tcp/dev-mathiasm:19106"
+cluster.rtc[2].part 1
+cluster.rtc[2].row 0
+cluster.rtc[2].feedservice "search/cluster.rt/c1/r0/feed-destination"
+cluster.rtc[3].spec "tcp/zarya:19157"
+cluster.rtc[3].part 1
+cluster.rtc[3].row 1
+cluster.rtc[3].feedservice "search/cluster.rt/c1/r1/feed-destination"
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..1407c3ff209
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/rtx/1/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19146
+slobrok.name "search/cluster.rt/rtx/1/clustercontroller"
+slobrok.config search/cluster.rt/rtx
+servicemonitor.autodisable true
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/fdispatchrc.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/fdispatchrc.MODEL.cfg
new file mode 100644
index 00000000000..ac173575923
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/fdispatchrc.MODEL.cfg
@@ -0,0 +1,23 @@
+fnetlockwait 20.0
+defaultslowdocsumlimitbias 1.0
+defaultslowdocsumlimitfactor 2.0
+defaultslowquerylimitbias 1.0
+defaultslowquerylimitfactor 2.0
+frtport 19148
+healthport 19150
+indexswitchminsearchgrace 0.0
+indexswitchmindocsumgrace 0.0
+indexswitchmaxsearchgrace 0.0
+indexswitchmaxdocsumgrace 64.0
+maxsocksilent 50.0
+maxthreads 1000
+transportthreads 1
+partition 0
+ptport 19149
+transport ""
+transportnodelay true
+transportdirectwrite false
+packetcompresslimit 1024
+packetcompresslevel 3
+packetcompresstype LZ4
+dispatchlevel 0
diff --git a/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/partitions.MODEL.cfg b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/partitions.MODEL.cfg
new file mode 100644
index 00000000000..4ef408e01ce
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/cluster.rt/tlds/tld.0/partitions.MODEL.cfg
@@ -0,0 +1,66 @@
+dataset[0].id 0
+dataset[0].refcost 1
+dataset[0].partbits 8
+dataset[0].rowbits 1
+dataset[0].numparts 2
+dataset[0].firstpart 0
+dataset[0].minpartitions 0
+dataset[0].mpp 1
+dataset[0].maxnodesdownperfixedrow 0
+dataset[0].useroundrobinforfixedrow true
+dataset[0].allowsearchonwarmupnodes true
+dataset[0].beforewarmupdelay 600.0
+dataset[0].engine[0].name_and_port "tcp/zarya:19152"
+dataset[0].engine[0].partid 0
+dataset[0].engine[0].rowid 0
+dataset[0].engine[0].subdatasetid 0
+dataset[0].engine[0].refcost 1
+dataset[0].engine[0].overridepartids true
+dataset[0].engine[1].name_and_port "tcp/dev-mathiasm:19101"
+dataset[0].engine[1].partid 0
+dataset[0].engine[1].rowid 1
+dataset[0].engine[1].subdatasetid 0
+dataset[0].engine[1].refcost 1
+dataset[0].engine[1].overridepartids true
+dataset[0].engine[2].name_and_port "tcp/dev-mathiasm:19107"
+dataset[0].engine[2].partid 1
+dataset[0].engine[2].rowid 0
+dataset[0].engine[2].subdatasetid 0
+dataset[0].engine[2].refcost 1
+dataset[0].engine[2].overridepartids true
+dataset[0].engine[3].name_and_port "tcp/zarya:19158"
+dataset[0].engine[3].partid 1
+dataset[0].engine[3].rowid 1
+dataset[0].engine[3].subdatasetid 0
+dataset[0].engine[3].refcost 1
+dataset[0].engine[3].overridepartids true
+dataset[0].maxhitspernode 2147483647
+dataset[0].estparts 0
+dataset[0].estpartcutoff 0
+dataset[0].minactive 500
+dataset[0].maxactive 500
+dataset[0].cutoffactive 1000
+dataset[0].minestactive 500
+dataset[0].maxestactive 1000
+dataset[0].cutoffestactive 1000
+dataset[0].queuedrainrate 400.0
+dataset[0].queuedrainmax 40.0
+dataset[0].slowquerylimitfactor 0.0
+dataset[0].slowquerylimitbias 100.0
+dataset[0].slowdocsumlimitfactor 0.0
+dataset[0].slowdocsumlimitbias 100.0
+dataset[0].monitorinterval 1.0
+dataset[0].higher_coverage_maxsearchwait 1.0
+dataset[0].higher_coverage_minsearchwait 0.0
+dataset[0].higher_coverage_basesearchwait 0.1
+dataset[0].minimal_searchcoverage 100.0
+dataset[0].higher_coverage_maxdocsumwait 0.3
+dataset[0].higher_coverage_mindocsumwait 0.1
+dataset[0].higher_coverage_basedocsumwait 0.1
+dataset[0].minimal_docsumcoverage 100.0
+dataset[0].querydistribution AUTOMATIC
+dataset[0].min_group_coverage 100.0
+dataset[0].min_activedocs_coverage 97.0
+dataset[0].latency_decay_rate 10000.0
+dataset[0].querydistributionsamplesize 10000
+dataset[0].querydistributionconfidenceinterval 99.9
diff --git a/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.0/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.1/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.1/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.1/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.2/.gitignore b/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.2/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/complex/search/qrservers/qrserver.2/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/optionals/hosts/zarya/sentinel/.gitignore b/config-model/src/test/cfg/search/compare/optionals/hosts/zarya/sentinel/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/hosts/zarya/sentinel/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/c0/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/c0/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..c5b1dfef610
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/c0/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19125
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/g0/c0/r0/.gitignore b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/g0/c0/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/g0/c0/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..d0888e9be96
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19115
+slobrok.name "search/cluster.music/rtx/0/clustercontroller"
+slobrok.config search/cluster.music/rtx
+servicemonitor.autodisable false
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg
new file mode 100644
index 00000000000..aa48d5fec79
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg
@@ -0,0 +1,23 @@
+fnetlockwait 20.0
+defaultslowdocsumlimitbias 1.0
+defaultslowdocsumlimitfactor 2.0
+defaultslowquerylimitbias 1.0
+defaultslowquerylimitfactor 2.0
+frtport 19117
+healthport 19119
+indexswitchminsearchgrace 0.0
+indexswitchmindocsumgrace 0.0
+indexswitchmaxsearchgrace 0.0
+indexswitchmaxdocsumgrace 64.0
+maxsocksilent 50.0
+maxthreads 1000
+transportthreads 1
+partition 0
+ptport 19118
+transport ""
+transportnodelay true
+transportdirectwrite false
+packetcompresslimit 1024
+packetcompresslevel 3
+packetcompresstype LZ4
+dispatchlevel 0
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg
new file mode 100644
index 00000000000..da5203914cb
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg
@@ -0,0 +1,48 @@
+dataset[0].id 0
+dataset[0].refcost 1
+dataset[0].partbits 8
+dataset[0].rowbits 0
+dataset[0].numparts 1
+dataset[0].firstpart 0
+dataset[0].minpartitions 0
+dataset[0].mpp 1
+dataset[0].maxnodesdownperfixedrow 0
+dataset[0].useroundrobinforfixedrow true
+dataset[0].allowsearchonwarmupnodes true
+dataset[0].beforewarmupdelay 600.0
+dataset[0].engine[0].name_and_port "tcp/zarya:19121"
+dataset[0].engine[0].partid 0
+dataset[0].engine[0].rowid 0
+dataset[0].engine[0].subdatasetid 0
+dataset[0].engine[0].refcost 1
+dataset[0].engine[0].overridepartids true
+dataset[0].maxhitspernode 2147483647
+dataset[0].estparts 0
+dataset[0].estpartcutoff 0
+dataset[0].minactive 500
+dataset[0].maxactive 500
+dataset[0].cutoffactive 1000
+dataset[0].minestactive 500
+dataset[0].maxestactive 1000
+dataset[0].cutoffestactive 1000
+dataset[0].queuedrainrate 400.0
+dataset[0].queuedrainmax 40.0
+dataset[0].slowquerylimitfactor 0.0
+dataset[0].slowquerylimitbias 100.0
+dataset[0].slowdocsumlimitfactor 0.0
+dataset[0].slowdocsumlimitbias 100.0
+dataset[0].monitorinterval 1.0
+dataset[0].higher_coverage_maxsearchwait 1.0
+dataset[0].higher_coverage_minsearchwait 0.0
+dataset[0].higher_coverage_basesearchwait 0.1
+dataset[0].minimal_searchcoverage 100.0
+dataset[0].higher_coverage_maxdocsumwait 0.3
+dataset[0].higher_coverage_mindocsumwait 0.1
+dataset[0].higher_coverage_basedocsumwait 0.1
+dataset[0].minimal_docsumcoverage 100.0
+dataset[0].querydistribution AUTOMATIC
+dataset[0].min_group_coverage 100.0
+dataset[0].min_activedocs_coverage 97.0
+dataset[0].latency_decay_rate 10000.0
+dataset[0].querydistributionsamplesize 10000
+dataset[0].querydistributionconfidenceinterval 99.9
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.0/.gitignore b/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.1/.gitignore b/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.1/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.1/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.2/.gitignore b/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.2/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/optionals/search/qrservers/qrserver.2/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/hosts/zarya/sentinel/.gitignore b/config-model/src/test/cfg/search/compare/simple/hosts/zarya/sentinel/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/hosts/zarya/sentinel/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/search/cluster.music/c0/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/c0/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..53dcc3f9686
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/c0/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19118
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/simple/search/cluster.music/g0/c0/r0/.gitignore b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/g0/c0/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/g0/c0/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx-rtlogic.MODEL.cfg b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx-rtlogic.MODEL.cfg
new file mode 100644
index 00000000000..d8d30dc5af3
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx-rtlogic.MODEL.cfg
@@ -0,0 +1,7 @@
+parts 1
+minnodesperpart 1
+maxbadparts 0
+cluster.rtc[0].spec "tcp/zarya:19113"
+cluster.rtc[0].part 0
+cluster.rtc[0].row 0
+cluster.rtc[0].feedservice "search/cluster.music/c0/r0/feed-destination"
diff --git a/config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..474b8c68cdb
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/rtx/0/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19108
+slobrok.name "search/cluster.music/rtx/0/clustercontroller"
+slobrok.config search/cluster.music/rtx
+servicemonitor.autodisable false
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg
new file mode 100644
index 00000000000..ce9e77ffdbc
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/fdispatchrc.MODEL.cfg
@@ -0,0 +1,23 @@
+fnetlockwait 20.0
+defaultslowdocsumlimitbias 1.0
+defaultslowdocsumlimitfactor 2.0
+defaultslowquerylimitbias 1.0
+defaultslowquerylimitfactor 2.0
+frtport 19110
+healthport 19112
+indexswitchminsearchgrace 0.0
+indexswitchmindocsumgrace 0.0
+indexswitchmaxsearchgrace 0.0
+indexswitchmaxdocsumgrace 64.0
+maxsocksilent 50.0
+maxthreads 1000
+transportthreads 1
+partition 0
+ptport 19111
+transport ""
+transportnodelay true
+transportdirectwrite false
+packetcompresslimit 1024
+packetcompresslevel 3
+packetcompresstype LZ4
+dispatchlevel 0
diff --git a/config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg
new file mode 100644
index 00000000000..e1c2253e6a9
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/cluster.music/tlds/tld.0/partitions.MODEL.cfg
@@ -0,0 +1,48 @@
+dataset[0].id 0
+dataset[0].refcost 1
+dataset[0].partbits 8
+dataset[0].rowbits 0
+dataset[0].numparts 1
+dataset[0].firstpart 0
+dataset[0].minpartitions 0
+dataset[0].mpp 1
+dataset[0].maxnodesdownperfixedrow 0
+dataset[0].useroundrobinforfixedrow true
+dataset[0].allowsearchonwarmupnodes true
+dataset[0].beforewarmupdelay 600.0
+dataset[0].engine[0].name_and_port "tcp/zarya:19114"
+dataset[0].engine[0].partid 0
+dataset[0].engine[0].rowid 0
+dataset[0].engine[0].subdatasetid 0
+dataset[0].engine[0].refcost 1
+dataset[0].engine[0].overridepartids true
+dataset[0].maxhitspernode 2147483647
+dataset[0].estparts 0
+dataset[0].estpartcutoff 0
+dataset[0].minactive 500
+dataset[0].maxactive 500
+dataset[0].cutoffactive 1000
+dataset[0].minestactive 500
+dataset[0].maxestactive 1000
+dataset[0].cutoffestactive 1000
+dataset[0].queuedrainrate 400.0
+dataset[0].queuedrainmax 40.0
+dataset[0].slowquerylimitfactor 0.0
+dataset[0].slowquerylimitbias 100.0
+dataset[0].slowdocsumlimitfactor 0.0
+dataset[0].slowdocsumlimitbias 100.0
+dataset[0].monitorinterval 1.0
+dataset[0].higher_coverage_maxsearchwait 1.0
+dataset[0].higher_coverage_minsearchwait 0.0
+dataset[0].higher_coverage_basesearchwait 0.1
+dataset[0].minimal_searchcoverage 100.0
+dataset[0].higher_coverage_maxdocsumwait 0.3
+dataset[0].higher_coverage_mindocsumwait 0.1
+dataset[0].higher_coverage_basedocsumwait 0.1
+dataset[0].minimal_docsumcoverage 100.0
+dataset[0].querydistribution AUTOMATIC
+dataset[0].min_group_coverage 100.0
+dataset[0].min_activedocs_coverage 97.0
+dataset[0].latency_decay_rate 10000.0
+dataset[0].querydistributionsamplesize 10000
+dataset[0].querydistributionconfidenceinterval 99.9
diff --git a/config-model/src/test/cfg/search/compare/simple/search/cluster.streaming/.gitignore b/config-model/src/test/cfg/search/compare/simple/search/cluster.streaming/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/cluster.streaming/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/search/qrservers/qrserver.0/.gitignore b/config-model/src/test/cfg/search/compare/simple/search/qrservers/qrserver.0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/search/qrservers/qrserver.0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/client/.gitignore b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/client/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/client/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/distributor/0/.gitignore b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/distributor/0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/distributor/0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/fleetcontroller/0/.gitignore b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/fleetcontroller/0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/fleetcontroller/0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/gateway/0/.gitignore b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/gateway/0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/gateway/0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/storage/0/.gitignore b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/storage/0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/simple/storage/cluster.storage/storage/0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/hosts/zarya/sentinel/.gitignore b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/hosts/zarya/sentinel/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/hosts/zarya/sentinel/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/c0/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/c0/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..6dabdfc6af7
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/c0/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19115
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/g0/c0/r0/.gitignore b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/g0/c0/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/g0/c0/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx-rtlogic.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx-rtlogic.MODEL.cfg
new file mode 100644
index 00000000000..2a20e0bca1e
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx-rtlogic.MODEL.cfg
@@ -0,0 +1,7 @@
+parts 1
+minnodesperpart 1
+maxbadparts 0
+cluster.rtc[0].spec "tcp/zarya:19110"
+cluster.rtc[0].part 0
+cluster.rtc[0].row 0
+cluster.rtc[0].feedservice "search/cluster.music1/c0/r0/feed-destination"
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..3c1b537236a
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/rtx/0/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19105
+slobrok.name "search/cluster.music1/rtx/0/clustercontroller"
+slobrok.config search/cluster.music1/rtx
+servicemonitor.autodisable false
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/fdispatchrc.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/fdispatchrc.MODEL.cfg
new file mode 100644
index 00000000000..d4135d10175
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/fdispatchrc.MODEL.cfg
@@ -0,0 +1,23 @@
+fnetlockwait 20.0
+defaultslowdocsumlimitbias 1.0
+defaultslowdocsumlimitfactor 2.0
+defaultslowquerylimitbias 1.0
+defaultslowquerylimitfactor 2.0
+frtport 19107
+healthport 19109
+indexswitchminsearchgrace 0.0
+indexswitchmindocsumgrace 0.0
+indexswitchmaxsearchgrace 0.0
+indexswitchmaxdocsumgrace 64.0
+maxsocksilent 50.0
+maxthreads 1000
+transportthreads 1
+partition 0
+ptport 19108
+transport ""
+transportnodelay true
+transportdirectwrite false
+packetcompresslimit 1024
+packetcompresslevel 3
+packetcompresstype LZ4
+dispatchlevel 0
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/partitions.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/partitions.MODEL.cfg
new file mode 100644
index 00000000000..ef99fc4f452
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music1/tlds/tld.0/partitions.MODEL.cfg
@@ -0,0 +1,48 @@
+dataset[0].id 0
+dataset[0].refcost 1
+dataset[0].partbits 8
+dataset[0].rowbits 0
+dataset[0].numparts 1
+dataset[0].firstpart 0
+dataset[0].minpartitions 0
+dataset[0].mpp 1
+dataset[0].maxnodesdownperfixedrow 0
+dataset[0].useroundrobinforfixedrow true
+dataset[0].allowsearchonwarmupnodes true
+dataset[0].beforewarmupdelay 600.0
+dataset[0].engine[0].name_and_port "tcp/zarya:19111"
+dataset[0].engine[0].partid 0
+dataset[0].engine[0].rowid 0
+dataset[0].engine[0].subdatasetid 0
+dataset[0].engine[0].refcost 1
+dataset[0].engine[0].overridepartids true
+dataset[0].maxhitspernode 2147483647
+dataset[0].estparts 0
+dataset[0].estpartcutoff 0
+dataset[0].minactive 500
+dataset[0].maxactive 500
+dataset[0].cutoffactive 1000
+dataset[0].minestactive 500
+dataset[0].maxestactive 1000
+dataset[0].cutoffestactive 1000
+dataset[0].queuedrainrate 400.0
+dataset[0].queuedrainmax 40.0
+dataset[0].slowquerylimitfactor 0.0
+dataset[0].slowquerylimitbias 100.0
+dataset[0].slowdocsumlimitfactor 0.0
+dataset[0].slowdocsumlimitbias 100.0
+dataset[0].monitorinterval 1.0
+dataset[0].higher_coverage_maxsearchwait 1.0
+dataset[0].higher_coverage_minsearchwait 0.0
+dataset[0].higher_coverage_basesearchwait 0.1
+dataset[0].minimal_searchcoverage 100.0
+dataset[0].higher_coverage_maxdocsumwait 0.3
+dataset[0].higher_coverage_mindocsumwait 0.1
+dataset[0].higher_coverage_basedocsumwait 0.1
+dataset[0].minimal_docsumcoverage 100.0
+dataset[0].querydistribution AUTOMATIC
+dataset[0].min_group_coverage 100.0
+dataset[0].min_activedocs_coverage 97.0
+dataset[0].latency_decay_rate 10000.0
+dataset[0].querydistributionsamplesize 10000
+dataset[0].querydistributionconfidenceinterval 99.9
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r0/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r0/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..b6d62fc678b
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r0/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19126
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r1/translogserver.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r1/translogserver.MODEL.cfg
new file mode 100644
index 00000000000..887eaa6b634
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/c0/r1/translogserver.MODEL.cfg
@@ -0,0 +1,7 @@
+listenport 19132
+filesizemax 50000000
+servername "tls"
+basedir "tls"
+usefsync false
+maxthreads 4
+crcmethod xxh64
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r0/.gitignore b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r0/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r1/.gitignore b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r1/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/g0/c0/r1/.gitignore
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/rtx/0/pan-rtx.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/rtx/0/pan-rtx.MODEL.cfg
new file mode 100644
index 00000000000..c00fcc456d6
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/rtx/0/pan-rtx.MODEL.cfg
@@ -0,0 +1,6 @@
+port 19116
+slobrok.name "search/cluster.music2/rtx/0/clustercontroller"
+slobrok.config search/cluster.music2/rtx
+servicemonitor.autodisable false
+servicemonitor.autoenable false
+servicemonitor.timeout 120
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/fdispatchrc.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/fdispatchrc.MODEL.cfg
new file mode 100644
index 00000000000..ee8b6cdd963
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/fdispatchrc.MODEL.cfg
@@ -0,0 +1,23 @@
+fnetlockwait 20.0
+defaultslowdocsumlimitbias 1.0
+defaultslowdocsumlimitfactor 2.0
+defaultslowquerylimitbias 1.0
+defaultslowquerylimitfactor 2.0
+frtport 19118
+healthport 19120
+indexswitchminsearchgrace 0.0
+indexswitchmindocsumgrace 0.0
+indexswitchmaxsearchgrace 0.0
+indexswitchmaxdocsumgrace 64.0
+maxsocksilent 50.0
+maxthreads 1000
+transportthreads 1
+partition 0
+ptport 19119
+transport ""
+transportnodelay true
+transportdirectwrite false
+packetcompresslimit 1024
+packetcompresslevel 3
+packetcompresstype LZ4
+dispatchlevel 0
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/partitions.MODEL.cfg b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/partitions.MODEL.cfg
new file mode 100644
index 00000000000..a03c9e58557
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/cluster.music2/tlds/tld.0/partitions.MODEL.cfg
@@ -0,0 +1,54 @@
+dataset[0].id 0
+dataset[0].refcost 1
+dataset[0].partbits 8
+dataset[0].rowbits 1
+dataset[0].numparts 1
+dataset[0].firstpart 0
+dataset[0].minpartitions 0
+dataset[0].mpp 1
+dataset[0].maxnodesdownperfixedrow 0
+dataset[0].useroundrobinforfixedrow true
+dataset[0].allowsearchonwarmupnodes true
+dataset[0].beforewarmupdelay 600.0
+dataset[0].engine[0].name_and_port "tcp/zarya:19122"
+dataset[0].engine[0].partid 0
+dataset[0].engine[0].rowid 0
+dataset[0].engine[0].subdatasetid 0
+dataset[0].engine[0].refcost 1
+dataset[0].engine[0].overridepartids true
+dataset[0].engine[1].name_and_port "tcp/zarya:19128"
+dataset[0].engine[1].partid 0
+dataset[0].engine[1].rowid 1
+dataset[0].engine[1].subdatasetid 0
+dataset[0].engine[1].refcost 1
+dataset[0].engine[1].overridepartids true
+dataset[0].maxhitspernode 2147483647
+dataset[0].estparts 0
+dataset[0].estpartcutoff 0
+dataset[0].minactive 500
+dataset[0].maxactive 500
+dataset[0].cutoffactive 1000
+dataset[0].minestactive 500
+dataset[0].maxestactive 1000
+dataset[0].cutoffestactive 1000
+dataset[0].queuedrainrate 400.0
+dataset[0].queuedrainmax 40.0
+dataset[0].slowquerylimitfactor 0.0
+dataset[0].slowquerylimitbias 100.0
+dataset[0].slowdocsumlimitfactor 0.0
+dataset[0].slowdocsumlimitbias 100.0
+dataset[0].monitorinterval 1.0
+dataset[0].higher_coverage_maxsearchwait 1.0
+dataset[0].higher_coverage_minsearchwait 0.0
+dataset[0].higher_coverage_basesearchwait 0.1
+dataset[0].minimal_searchcoverage 100.0
+dataset[0].higher_coverage_maxdocsumwait 0.3
+dataset[0].higher_coverage_mindocsumwait 0.1
+dataset[0].higher_coverage_basedocsumwait 0.1
+dataset[0].minimal_docsumcoverage 100.0
+dataset[0].querydistribution AUTOMATIC
+dataset[0].min_group_coverage 100.0
+dataset[0].min_activedocs_coverage 97.0
+dataset[0].latency_decay_rate 10000.0
+dataset[0].querydistributionsamplesize 10000
+dataset[0].querydistributionconfidenceinterval 99.9
diff --git a/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/qrservers/qrserver.0/.gitignore b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/qrservers/qrserver.0/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/cfg/search/compare/twoFeedTargetClusters/search/qrservers/qrserver.0/.gitignore
diff --git a/config-model/src/test/cfg/search/data/nextgen-simple-v2/searchdefinitions/nextgendoc.sd b/config-model/src/test/cfg/search/data/nextgen-simple-v2/searchdefinitions/nextgendoc.sd
new file mode 100644
index 00000000000..461e7123c01
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/nextgen-simple-v2/searchdefinitions/nextgendoc.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search nextgendoc {
+ document nextgendoc {
+ field body type string {
+ indexing: summary | index
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/search/data/nextgen-simple-v2/services.xml b/config-model/src/test/cfg/search/data/nextgen-simple-v2/services.xml
new file mode 100644
index 00000000000..cce1cf40419
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/nextgen-simple-v2/services.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+
+ <adminserver hostalias="node1" />
+
+ <logserver hostalias="node1" />
+ <slobroks>
+ <slobrok hostalias="node1" />
+ </slobroks>
+ </admin>
+
+ <search version="2.0">
+ <qrservers>
+ <qrserver hostalias="node1" />
+ </qrservers>
+ <cluster name="search" indexingmode="realtime">
+ <searchdefinitions>
+ <searchdefinition name="nextgendoc" />
+ </searchdefinitions>
+ <clustercontrollers>
+ <clustercontroller hostalias="node1" />
+ </clustercontrollers>
+ <topleveldispatchers>
+ <topleveldispatcher hostalias="node1" />
+ </topleveldispatchers>
+ <row index="0">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0" />
+ </searchnodes>
+ </row>
+ </cluster>
+ </search>
+
+</services>
diff --git a/config-model/src/test/cfg/search/data/onlybundles/components/testbundle.jar b/config-model/src/test/cfg/search/data/onlybundles/components/testbundle.jar
new file mode 100644
index 00000000000..69f6e335092
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/onlybundles/components/testbundle.jar
Binary files differ
diff --git a/config-model/src/test/cfg/search/data/onlybundles/services.xml b/config-model/src/test/cfg/search/data/onlybundles/services.xml
new file mode 100644
index 00000000000..427b0c6d23f
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/onlybundles/services.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1"/>
+ </admin>
+
+ <search version="2.0">
+ <qrservers>
+ <qrserver hostalias="node1" />
+ </qrservers>
+ <cluster name="music" indexingmode="realtime">
+ <visibilitydelay>15.7</visibilitydelay>
+ <searchdefinitions>
+ <searchdefinition name="music2" />
+ </searchdefinitions>
+ <documents selection="music" />
+ <clustercontrollers>
+ <clustercontroller hostalias="node1" />
+ </clustercontrollers>
+ <topleveldispatchers>
+ <topleveldispatcher hostalias="node1" />
+ </topleveldispatchers>
+ <row index="0">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0" />
+ </searchnodes>
+ </row>
+ </cluster>
+
+ </search>
+
+</services>
diff --git a/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTData.sd b/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTData.sd
new file mode 100644
index 00000000000..274b03f247a
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTData.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Basic search definition for Travel Attraction (self) facet
+
+search TTData {
+ document TTData inherits TTPOI,TTEdge {
+
+ }
+
+}
+
diff --git a/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTEdge.sd b/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTEdge.sd
new file mode 100644
index 00000000000..ce22f2f77ec
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTEdge.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document TTEdge {
+
+# This field will contain a colon separate map for travel times per transport mode
+ field TransportMode type array<string> {
+ indexing: summary | index
+ header
+ }
+}
+
diff --git a/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTPOI.sd b/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTPOI.sd
new file mode 100644
index 00000000000..d8308df4c7f
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/travel/searchdefinitions/TTPOI.sd
@@ -0,0 +1,19 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document TTPOI {
+
+# categories associated with the POI
+ field Categories type array<string> {
+ indexing: summary | index
+ # index-to: Categories
+ header
+ }
+
+# sub catagories associated with the POI
+ field SubCategories type array<string> {
+ indexing: summary | index
+ # index-to: SubCategories
+ header
+ }
+}
+
+
diff --git a/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/base.sd b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/base.sd
new file mode 100644
index 00000000000..5de7b537439
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/base.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search base {
+ document base {
+ field fbase type string {
+ indexing: summary | index
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/left.sd b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/left.sd
new file mode 100644
index 00000000000..ebb0a8e4631
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/left.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search left {
+ document left {
+ field fleft type string {
+ indexing: summary | index
+ }
+ }
+
+ rank-profile base inherits default {
+ first-phase {
+ expression: fleft
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/music.sd b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/music.sd
new file mode 100644
index 00000000000..4b78eeae4ab
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/music.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ header
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ body
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/right.sd b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/right.sd
new file mode 100644
index 00000000000..cf824ad102f
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/searchdefinitions/right.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search right {
+ document right {
+ field fright type string {
+ indexing: summary | index
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/services.xml b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/services.xml
new file mode 100644
index 00000000000..c5cfd7f7d79
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/inherited_rankprofiles/services.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <content version="1.0" id="inherit">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="left" mode="index" />
+ <document type="right" mode="index" />
+ </documents>
+ <engine>
+ <proton />
+ </engine>
+ <nodes>
+ <node hostalias="node0" distribution-key="0" />
+ </nodes>
+ </content>
+</services>
diff --git a/config-model/src/test/cfg/search/data/v2/modularsearchchains/hosts.xml b/config-model/src/test/cfg/search/data/v2/modularsearchchains/hosts.xml
new file mode 100644
index 00000000000..e2b97e374e6
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/modularsearchchains/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="localhost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain2.xml b/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain2.xml
new file mode 100644
index 00000000000..cbb43e26ec7
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain2.xml
@@ -0,0 +1,9 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<searchchains>
+<searcher class="com.yahoo.search.example.outsidechain2.SimpleSearcher" id="s1" bundle="mybundle"/>
+
+<searchchain id="chain2">
+ <searcher id="s1"/>
+ <searcher id="com.yahoo.search.example.chain2.SimpleSearcher2" bundle="mybundle"/>
+</searchchain>
+</searchchains>
diff --git a/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain3.xml b/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain3.xml
new file mode 100644
index 00000000000..c17a84407a1
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/chain3.xml
@@ -0,0 +1,10 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<searchchains>
+<searchchain id="chain3_1">
+ <searcher id="com.yahoo.search.example.chain3_1.SimpleSearcher" bundle="mybundle"/>
+</searchchain>
+<searchchain id="chain3_2">
+ <searcher id="com.yahoo.search.example.chain3_2.SimpleSearcher" bundle="mybundle"/>
+ <searcher id="com.yahoo.search.example.chain3_2.SimpleSearcher2" bundle="mybundle"/>
+</searchchain>
+</searchchains>
diff --git a/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/default.xml b/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/default.xml
new file mode 100644
index 00000000000..65c59939c49
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/modularsearchchains/search/chains/default.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<searchchains>
+<searchchain id="default">
+ <searcher id="com.yahoo.search.example.default.SimpleSearcher" bundle="mybundle"/>
+</searchchain>
+</searchchains>
diff --git a/config-model/src/test/cfg/search/data/v2/modularsearchchains/services.xml b/config-model/src/test/cfg/search/data/v2/modularsearchchains/services.xml
new file mode 100644
index 00000000000..15cc4eca2ec
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/modularsearchchains/services.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+
+ <adminserver hostalias="node1"/>
+
+ <logserver hostalias="node1"/>
+ </admin>
+
+ <search version="2.0">
+ <qrservers>
+ <qrserver hostalias="node1"/>
+ <searchchains>
+ <searcher id="com.yahoo.search.example.inline.SimpleSearcher3" bundle="mybundle"/>
+ <searchchain id="inline">
+ <searcher id="com.yahoo.search.example.inline.SimpleSearcher" bundle="mybundle"/>
+ <searcher id="com.yahoo.search.example.inline.SimpleSearcher2" bundle="mybundle"/>
+ </searchchain>
+ </searchchains>
+ </qrservers>
+ </search>
+
+</services>
diff --git a/config-model/src/test/cfg/search/data/v2/onlybundles/components/testbundle.jar b/config-model/src/test/cfg/search/data/v2/onlybundles/components/testbundle.jar
new file mode 100644
index 00000000000..69f6e335092
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/onlybundles/components/testbundle.jar
Binary files differ
diff --git a/config-model/src/test/cfg/search/data/v2/onlybundles/services.xml b/config-model/src/test/cfg/search/data/v2/onlybundles/services.xml
new file mode 100644
index 00000000000..7d5048ba9a2
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/onlybundles/services.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1"/>
+ </admin>
+
+ <search version="2.0">
+ <qrservers>
+ <qrserver hostalias="node1"/>
+ </qrservers>
+ <cluster name="music">
+ <visibilitydelay>15.7</visibilitydelay>
+ <searchdefinitions>
+ <searchdefinition name="music2"/>
+ </searchdefinitions>
+ <documents selection="music"/>
+ <clustercontrollers>
+ <clustercontroller hostalias="node1"/>
+ </clustercontrollers>
+ <topleveldispatchers>
+ <topleveldispatcher hostalias="node1"/>
+ </topleveldispatchers>
+ <row index="0">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0"/>
+ </searchnodes>
+ </row>
+ </cluster>
+
+ </search>
+
+</services>
diff --git a/config-model/src/test/cfg/search/data/v2/proton-yamas/hosts.xml b/config-model/src/test/cfg/search/data/v2/proton-yamas/hosts.xml
new file mode 100644
index 00000000000..9215f81ad98
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/proton-yamas/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="zarya">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/search/data/v2/proton-yamas/searchdefinitions/music.sd b/config-model/src/test/cfg/search/data/v2/proton-yamas/searchdefinitions/music.sd
new file mode 100644
index 00000000000..136efeafaf6
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/proton-yamas/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/search/data/v2/proton-yamas/services.xml b/config-model/src/test/cfg/search/data/v2/proton-yamas/services.xml
new file mode 100644
index 00000000000..226589e9835
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/proton-yamas/services.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1"/>
+ <yamas systemname="news_staging" interval="60"/>
+ </admin>
+
+ <container version="1.0">
+ <nodes>
+ <node hostalias="node1"/>
+ </nodes>
+ <search/>
+ </container>
+
+ <content id="music" version="1.0">
+
+ <redundancy>2</redundancy>
+
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+
+ <nodes>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/search/data/v2/stripped/services.xml b/config-model/src/test/cfg/search/data/v2/stripped/services.xml
new file mode 100644
index 00000000000..ba7ab8a052a
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/stripped/services.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1"/>
+ </admin>
+
+
+ <docproc version="3.0">
+ <docprocchains>
+ <docprocchain id="standalonechain">
+ <documentprocessor id="AppleDocProc"/>
+ </docprocchain>
+ </docprocchains>
+ </docproc>
+
+ <storage version="3.0">
+ <cluster redundancy="1">
+
+ <group index="0" name="0">
+ <node hostalias="node0" index="0"/>
+ </group>
+
+ <fleetcontrollers transitiontime="0">
+ <fleetcontroller hostalias="node0" index="0"/>
+ </fleetcontrollers>
+
+ </cluster>
+ </storage>
+
+</services>
diff --git a/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/hosts.xml b/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/hosts.xml
new file mode 100644
index 00000000000..9215f81ad98
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="zarya">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/searchdefinitions/music.sd b/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/searchdefinitions/music.sd
new file mode 100755
index 00000000000..392e19213b1
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/searchdefinitions/music.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/services.xml b/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/services.xml
new file mode 100644
index 00000000000..8c78597b5b5
--- /dev/null
+++ b/config-model/src/test/cfg/search/data/v2/twoFeedTargetClusters/services.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1"/>
+ </admin>
+ <search version="2.0">
+ <qrservers>
+ <qrserver hostalias="node1"/>
+ </qrservers>
+ <cluster name="music1">
+ <searchdefinitions>
+ <searchdefinition name="music"/>
+ </searchdefinitions>
+ <!-- The point with this test is to have a 'documents' tag without a 'feedname' attribute -->
+ <documents selection="music.f1"/>
+ <clustercontrollers>
+ <clustercontroller hostalias="node1"/>
+ </clustercontrollers>
+ <topleveldispatchers>
+ <topleveldispatcher hostalias="node1"/>
+ </topleveldispatchers>
+ <row index="0">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0"/>
+ </searchnodes>
+ </row>
+ </cluster>
+
+ <cluster name="music2">
+ <searchdefinitions>
+ <searchdefinition name="music"/>
+ </searchdefinitions>
+ <!-- The point with this test is to have a 'documents' tag without a 'feedname' attribute -->
+ <documents selection="music.f2"/>
+ <clustercontrollers>
+ <clustercontroller hostalias="node1"/>
+ </clustercontrollers>
+ <topleveldispatchers>
+ <topleveldispatcher hostalias="node1"/>
+ </topleveldispatchers>
+ <row index="0">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0"/>
+ </searchnodes>
+ </row>
+ <row index="1">
+ <searchnodes>
+ <searchnode hostalias="node1" index="0"/>
+ </searchnodes>
+ </row>
+ </cluster>
+ </search>
+</services>
diff --git a/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/hosts.xml b/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/hosts.xml
new file mode 100644
index 00000000000..20665c8bbd1
--- /dev/null
+++ b/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/hosts.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="mynode1">
+ <alias>node0</alias>
+ </host>
+
+ <host name="mynode2">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/searchdefinitions/music.sd b/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/searchdefinitions/music.sd
new file mode 100644
index 00000000000..4b78eeae4ab
--- /dev/null
+++ b/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/searchdefinitions/music.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ header
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ body
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/services.xml b/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/services.xml
new file mode 100644
index 00000000000..e0202dc124a
--- /dev/null
+++ b/config-model/src/test/cfg/storage/app_index_higher_than_num_nodes/services.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="node0"/>
+ <logserver hostalias="node0"/>
+ </admin>
+
+ <content version="1.0">
+ <redundancy>2</redundancy>
+
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+
+ <tuning>
+ <cluster-controller>
+ <transition-time>60</transition-time>
+ </cluster-controller>
+ </tuning>
+
+ <group name="mycluster">
+ <node hostalias="node0" distribution-key="0" capacity="0.79"/>
+ <node hostalias="node1" distribution-key="2"/>
+ </group>
+
+ </content>
+
+</services>
diff --git a/config-model/src/test/cfg/storage/clustercontroller_advanced/hosts.xml b/config-model/src/test/cfg/storage/clustercontroller_advanced/hosts.xml
new file mode 100644
index 00000000000..29e61b6abc6
--- /dev/null
+++ b/config-model/src/test/cfg/storage/clustercontroller_advanced/hosts.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="mynode1">
+ <alias>node0</alias>
+ </host>
+
+ <host name="mynode2">
+ <alias>node1</alias>
+ </host>
+
+ <host name="mynode3">
+ <alias>node2</alias>
+ </host>
+</hosts>
diff --git a/config-model/src/test/cfg/storage/clustercontroller_advanced/searchdefinitions/music.sd b/config-model/src/test/cfg/storage/clustercontroller_advanced/searchdefinitions/music.sd
new file mode 100644
index 00000000000..4b78eeae4ab
--- /dev/null
+++ b/config-model/src/test/cfg/storage/clustercontroller_advanced/searchdefinitions/music.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ field f1 type string {
+ indexing: summary | index
+ # index-to: f1, all
+ header
+ }
+ field f2 type string {
+ indexing: summary | index
+ # index-to: f2, all
+ body
+ }
+ }
+}
diff --git a/config-model/src/test/cfg/storage/clustercontroller_advanced/services.xml b/config-model/src/test/cfg/storage/clustercontroller_advanced/services.xml
new file mode 100644
index 00000000000..9a07e1c3e87
--- /dev/null
+++ b/config-model/src/test/cfg/storage/clustercontroller_advanced/services.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services>
+
+ <admin version="2.0">
+ <adminserver hostalias="node0"/>
+ <logserver hostalias="node0"/>
+ <yamas interval="60"/>
+ </admin>
+
+ <content version="1.0">
+ <redundancy>2</redundancy>
+
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+
+ <tuning>
+ <cluster-controller>
+ <transition-time>5</transition-time>
+ <init-progress-time>2</init-progress-time>
+ <max-premature-crashes>3</max-premature-crashes>
+ <stable-state-period>240</stable-state-period>
+ <min-distributor-up-ratio>0.0</min-distributor-up-ratio>
+ <min-storage-up-ratio>0.7</min-storage-up-ratio>
+ </cluster-controller>
+ </tuning>
+
+ <group name="mycluster">
+ <node hostalias="node0" distribution-key="0"/>
+ <node hostalias="node1" distribution-key="1"/>
+ <node hostalias="node2" distribution-key="2"/>
+ </group>
+
+ </content>
+
+</services>
diff --git a/config-model/src/test/configmodel/types/documentmanager.cfg b/config-model/src/test/configmodel/types/documentmanager.cfg
new file mode 100644
index 00000000000..ac148209d6b
--- /dev/null
+++ b/config-model/src/test/configmodel/types/documentmanager.cfg
@@ -0,0 +1,221 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -1865479609
+datatype[1].maptype[0].keytype 2
+datatype[1].maptype[0].valtype 4
+datatype[2].id 294108848
+datatype[2].structtype[0].name "folder"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "Version"
+datatype[2].structtype[0].field[0].datatype 0
+datatype[2].structtype[0].field[1].name "Name"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[2].structtype[0].field[2].name "FlagsCounter"
+datatype[2].structtype[0].field[2].datatype -1865479609
+datatype[2].structtype[0].field[3].name "anotherfolder"
+datatype[2].structtype[0].field[3].datatype 294108848
+datatype[3].id 109267174
+datatype[3].structtype[0].name "sct"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "s1"
+datatype[3].structtype[0].field[0].datatype 2
+datatype[3].structtype[0].field[1].name "s2"
+datatype[3].structtype[0].field[1].datatype 2
+datatype[4].id 49942803
+datatype[4].arraytype[0].datatype 16
+datatype[5].id 339965458
+datatype[5].maptype[0].keytype 2
+datatype[5].maptype[0].valtype 2
+datatype[6].id -2092985853
+datatype[6].structtype[0].name "mystruct"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[6].structtype[0].field[0].name "bytearr"
+datatype[6].structtype[0].field[0].datatype 49942803
+datatype[6].structtype[0].field[1].name "mymap"
+datatype[6].structtype[0].field[1].datatype 339965458
+datatype[6].structtype[0].field[2].name "title"
+datatype[6].structtype[0].field[2].datatype 2
+datatype[6].structtype[0].field[3].name "structfield"
+datatype[6].structtype[0].field[3].datatype 2
+datatype[7].id -1245117006
+datatype[7].arraytype[0].datatype 0
+datatype[8].id 1328286588
+datatype[8].weightedsettype[0].datatype 2
+datatype[8].weightedsettype[0].createifnonexistant false
+datatype[8].weightedsettype[0].removeifzero false
+datatype[9].id 2125328771
+datatype[9].weightedsettype[0].datatype 2
+datatype[9].weightedsettype[0].createifnonexistant false
+datatype[9].weightedsettype[0].removeifzero true
+datatype[10].id 2065577986
+datatype[10].weightedsettype[0].datatype 2
+datatype[10].weightedsettype[0].createifnonexistant true
+datatype[10].weightedsettype[0].removeifzero false
+datatype[11].id -1244829667
+datatype[11].arraytype[0].datatype 109267174
+datatype[12].id -1584287606
+datatype[12].maptype[0].keytype 2
+datatype[12].maptype[0].valtype 0
+datatype[13].id 2125154557
+datatype[13].maptype[0].keytype 2
+datatype[13].maptype[0].valtype 1
+datatype[14].id -1715531035
+datatype[14].maptype[0].keytype 0
+datatype[14].maptype[0].valtype 4
+datatype[15].id 2138385264
+datatype[15].maptype[0].keytype 0
+datatype[15].maptype[0].valtype 5
+datatype[16].id 435886609
+datatype[16].maptype[0].keytype 2
+datatype[16].maptype[0].valtype -1245117006
+datatype[17].id -1486737430
+datatype[17].arraytype[0].datatype 2
+datatype[18].id 1707615575
+datatype[18].arraytype[0].datatype -1486737430
+datatype[19].id -794985308
+datatype[19].arraytype[0].datatype 1707615575
+datatype[20].id 69621385
+datatype[20].arraytype[0].datatype 339965458
+datatype[21].id 1901258752
+datatype[21].maptype[0].keytype 0
+datatype[21].maptype[0].valtype -2092985853
+datatype[22].id 759956026
+datatype[22].arraytype[0].datatype -2092985853
+datatype[23].id -389833101
+datatype[23].maptype[0].keytype 0
+datatype[23].maptype[0].valtype 294108848
+datatype[24].id 1328581348
+datatype[24].structtype[0].name "types.header"
+datatype[24].structtype[0].version 0
+datatype[24].structtype[0].compresstype NONE
+datatype[24].structtype[0].compresslevel 0
+datatype[24].structtype[0].compressthreshold 95
+datatype[24].structtype[0].compressminsize 800
+datatype[24].structtype[0].field[0].name "abyte"
+datatype[24].structtype[0].field[0].datatype 16
+datatype[24].structtype[0].field[1].name "along"
+datatype[24].structtype[0].field[1].datatype 4
+datatype[24].structtype[0].field[2].name "arrayfield"
+datatype[24].structtype[0].field[2].datatype -1245117006
+datatype[24].structtype[0].field[3].name "setfield"
+datatype[24].structtype[0].field[3].datatype 1328286588
+datatype[24].structtype[0].field[4].name "pos"
+datatype[24].structtype[0].field[4].datatype 1381038251
+datatype[24].structtype[0].field[5].name "setfield2"
+datatype[24].structtype[0].field[5].datatype 18
+datatype[24].structtype[0].field[6].name "setfield3"
+datatype[24].structtype[0].field[6].datatype 2125328771
+datatype[24].structtype[0].field[7].name "setfield4"
+datatype[24].structtype[0].field[7].datatype 2065577986
+datatype[24].structtype[0].field[8].name "tagfield"
+datatype[24].structtype[0].field[8].datatype 18
+datatype[24].structtype[0].field[9].name "structfield"
+datatype[24].structtype[0].field[9].datatype 109267174
+datatype[24].structtype[0].field[10].name "structarrayfield"
+datatype[24].structtype[0].field[10].datatype -1244829667
+datatype[24].structtype[0].field[11].name "stringmapfield"
+datatype[24].structtype[0].field[11].datatype 339965458
+datatype[24].structtype[0].field[12].name "intmapfield"
+datatype[24].structtype[0].field[12].datatype -1584287606
+datatype[24].structtype[0].field[13].name "floatmapfield"
+datatype[24].structtype[0].field[13].datatype 2125154557
+datatype[24].structtype[0].field[14].name "longmapfield"
+datatype[24].structtype[0].field[14].datatype -1715531035
+datatype[24].structtype[0].field[15].name "doublemapfield"
+datatype[24].structtype[0].field[15].datatype 2138385264
+datatype[24].structtype[0].field[16].name "arraymapfield"
+datatype[24].structtype[0].field[16].datatype 435886609
+datatype[24].structtype[0].field[17].name "arrarr"
+datatype[24].structtype[0].field[17].datatype -794985308
+datatype[24].structtype[0].field[18].name "maparr"
+datatype[24].structtype[0].field[18].datatype 69621385
+datatype[24].structtype[0].field[19].name "mystructfield"
+datatype[24].structtype[0].field[19].datatype -2092985853
+datatype[24].structtype[0].field[20].name "mystructmap"
+datatype[24].structtype[0].field[20].datatype 1901258752
+datatype[24].structtype[0].field[21].name "mystructarr"
+datatype[24].structtype[0].field[21].datatype 759956026
+datatype[24].structtype[0].field[22].name "Folders"
+datatype[24].structtype[0].field[22].datatype -389833101
+datatype[24].structtype[0].field[23].name "juletre"
+datatype[24].structtype[0].field[23].datatype 4
+datatype[24].structtype[0].field[24].name "album0"
+datatype[24].structtype[0].field[24].datatype 18
+datatype[24].structtype[0].field[25].name "album1"
+datatype[24].structtype[0].field[25].datatype 18
+datatype[24].structtype[0].field[26].name "other"
+datatype[24].structtype[0].field[26].datatype 4
+datatype[24].structtype[0].field[27].name "rankfeatures"
+datatype[24].structtype[0].field[27].datatype 2
+datatype[24].structtype[0].field[28].name "summaryfeatures"
+datatype[24].structtype[0].field[28].datatype 2
+datatype[25].id 171503364
+datatype[25].maptype[0].keytype 1707615575
+datatype[25].maptype[0].valtype 0
+datatype[26].id 1100964733
+datatype[26].arraytype[0].datatype 171503364
+datatype[27].id 348447225
+datatype[27].structtype[0].name "types.body"
+datatype[27].structtype[0].version 0
+datatype[27].structtype[0].compresstype NONE
+datatype[27].structtype[0].compresslevel 0
+datatype[27].structtype[0].compressthreshold 95
+datatype[27].structtype[0].compressminsize 800
+datatype[27].structtype[0].field[0].name "complexarray"
+datatype[27].structtype[0].field[0].datatype 1100964733
+datatype[28].id -853072901
+datatype[28].documenttype[0].name "types"
+datatype[28].documenttype[0].version 0
+datatype[28].documenttype[0].inherits[0].name "document"
+datatype[28].documenttype[0].inherits[0].version 0
+datatype[28].documenttype[0].headerstruct 1328581348
+datatype[28].documenttype[0].bodystruct 348447225
+datatype[28].documenttype[0].fieldsets{[document]}.fields[0] "Folders"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[1] "abyte"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[2] "album0"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[3] "album1"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[4] "along"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[5] "arrarr"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[6] "arrayfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[7] "arraymapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[8] "complexarray"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[9] "doublemapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[10] "floatmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[11] "intmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[12] "juletre"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[13] "longmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[14] "maparr"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[15] "mystructarr"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[16] "mystructfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[17] "mystructmap"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[18] "pos"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[19] "setfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[20] "setfield2"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[21] "setfield3"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[22] "setfield4"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[23] "stringmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[24] "structarrayfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[25] "structfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[26] "tagfield"
diff --git a/config-model/src/test/configmodel/types/documenttypes.cfg b/config-model/src/test/configmodel/types/documenttypes.cfg
new file mode 100644
index 00000000000..eacd878e13d
--- /dev/null
+++ b/config-model/src/test/configmodel/types/documenttypes.cfg
@@ -0,0 +1,599 @@
+enablecompression false
+documenttype[0].id -853072901
+documenttype[0].name "types"
+documenttype[0].version 0
+documenttype[0].headerstruct 1328581348
+documenttype[0].bodystruct 348447225
+documenttype[0].inherits[0].id 8
+documenttype[0].datatype[0].id -1865479609
+documenttype[0].datatype[0].type MAP
+documenttype[0].datatype[0].array.element.id 0
+documenttype[0].datatype[0].map.key.id 2
+documenttype[0].datatype[0].map.value.id 4
+documenttype[0].datatype[0].wset.key.id 0
+documenttype[0].datatype[0].wset.createifnonexistent false
+documenttype[0].datatype[0].wset.removeifzero false
+documenttype[0].datatype[0].annotationref.annotation.id 0
+documenttype[0].datatype[0].sstruct.name ""
+documenttype[0].datatype[0].sstruct.version 0
+documenttype[0].datatype[0].sstruct.compression.type NONE
+documenttype[0].datatype[0].sstruct.compression.level 0
+documenttype[0].datatype[0].sstruct.compression.threshold 95
+documenttype[0].datatype[0].sstruct.compression.minsize 200
+documenttype[0].datatype[1].id 294108848
+documenttype[0].datatype[1].type STRUCT
+documenttype[0].datatype[1].array.element.id 0
+documenttype[0].datatype[1].map.key.id 0
+documenttype[0].datatype[1].map.value.id 0
+documenttype[0].datatype[1].wset.key.id 0
+documenttype[0].datatype[1].wset.createifnonexistent false
+documenttype[0].datatype[1].wset.removeifzero false
+documenttype[0].datatype[1].annotationref.annotation.id 0
+documenttype[0].datatype[1].sstruct.name "folder"
+documenttype[0].datatype[1].sstruct.version 0
+documenttype[0].datatype[1].sstruct.compression.type NONE
+documenttype[0].datatype[1].sstruct.compression.level 0
+documenttype[0].datatype[1].sstruct.compression.threshold 95
+documenttype[0].datatype[1].sstruct.compression.minsize 200
+documenttype[0].datatype[1].sstruct.field[0].name "Version"
+documenttype[0].datatype[1].sstruct.field[0].id 64430502
+documenttype[0].datatype[1].sstruct.field[0].id_v6 634243672
+documenttype[0].datatype[1].sstruct.field[0].datatype 0
+documenttype[0].datatype[1].sstruct.field[1].name "Name"
+documenttype[0].datatype[1].sstruct.field[1].id 2002760220
+documenttype[0].datatype[1].sstruct.field[1].id_v6 62942997
+documenttype[0].datatype[1].sstruct.field[1].datatype 2
+documenttype[0].datatype[1].sstruct.field[2].name "FlagsCounter"
+documenttype[0].datatype[1].sstruct.field[2].id 1741227606
+documenttype[0].datatype[1].sstruct.field[2].id_v6 1287497652
+documenttype[0].datatype[1].sstruct.field[2].datatype -1865479609
+documenttype[0].datatype[1].sstruct.field[3].name "anotherfolder"
+documenttype[0].datatype[1].sstruct.field[3].id 1582421848
+documenttype[0].datatype[1].sstruct.field[3].id_v6 1898725199
+documenttype[0].datatype[1].sstruct.field[3].datatype 294108848
+documenttype[0].datatype[2].id 109267174
+documenttype[0].datatype[2].type STRUCT
+documenttype[0].datatype[2].array.element.id 0
+documenttype[0].datatype[2].map.key.id 0
+documenttype[0].datatype[2].map.value.id 0
+documenttype[0].datatype[2].wset.key.id 0
+documenttype[0].datatype[2].wset.createifnonexistent false
+documenttype[0].datatype[2].wset.removeifzero false
+documenttype[0].datatype[2].annotationref.annotation.id 0
+documenttype[0].datatype[2].sstruct.name "sct"
+documenttype[0].datatype[2].sstruct.version 0
+documenttype[0].datatype[2].sstruct.compression.type NONE
+documenttype[0].datatype[2].sstruct.compression.level 0
+documenttype[0].datatype[2].sstruct.compression.threshold 95
+documenttype[0].datatype[2].sstruct.compression.minsize 200
+documenttype[0].datatype[2].sstruct.field[0].name "s1"
+documenttype[0].datatype[2].sstruct.field[0].id 2146820765
+documenttype[0].datatype[2].sstruct.field[0].id_v6 142373281
+documenttype[0].datatype[2].sstruct.field[0].datatype 2
+documenttype[0].datatype[2].sstruct.field[1].name "s2"
+documenttype[0].datatype[2].sstruct.field[1].id 45366795
+documenttype[0].datatype[2].sstruct.field[1].id_v6 31106270
+documenttype[0].datatype[2].sstruct.field[1].datatype 2
+documenttype[0].datatype[3].id 49942803
+documenttype[0].datatype[3].type ARRAY
+documenttype[0].datatype[3].array.element.id 16
+documenttype[0].datatype[3].map.key.id 0
+documenttype[0].datatype[3].map.value.id 0
+documenttype[0].datatype[3].wset.key.id 0
+documenttype[0].datatype[3].wset.createifnonexistent false
+documenttype[0].datatype[3].wset.removeifzero false
+documenttype[0].datatype[3].annotationref.annotation.id 0
+documenttype[0].datatype[3].sstruct.name ""
+documenttype[0].datatype[3].sstruct.version 0
+documenttype[0].datatype[3].sstruct.compression.type NONE
+documenttype[0].datatype[3].sstruct.compression.level 0
+documenttype[0].datatype[3].sstruct.compression.threshold 95
+documenttype[0].datatype[3].sstruct.compression.minsize 200
+documenttype[0].datatype[4].id 339965458
+documenttype[0].datatype[4].type MAP
+documenttype[0].datatype[4].array.element.id 0
+documenttype[0].datatype[4].map.key.id 2
+documenttype[0].datatype[4].map.value.id 2
+documenttype[0].datatype[4].wset.key.id 0
+documenttype[0].datatype[4].wset.createifnonexistent false
+documenttype[0].datatype[4].wset.removeifzero false
+documenttype[0].datatype[4].annotationref.annotation.id 0
+documenttype[0].datatype[4].sstruct.name ""
+documenttype[0].datatype[4].sstruct.version 0
+documenttype[0].datatype[4].sstruct.compression.type NONE
+documenttype[0].datatype[4].sstruct.compression.level 0
+documenttype[0].datatype[4].sstruct.compression.threshold 95
+documenttype[0].datatype[4].sstruct.compression.minsize 200
+documenttype[0].datatype[5].id -2092985853
+documenttype[0].datatype[5].type STRUCT
+documenttype[0].datatype[5].array.element.id 0
+documenttype[0].datatype[5].map.key.id 0
+documenttype[0].datatype[5].map.value.id 0
+documenttype[0].datatype[5].wset.key.id 0
+documenttype[0].datatype[5].wset.createifnonexistent false
+documenttype[0].datatype[5].wset.removeifzero false
+documenttype[0].datatype[5].annotationref.annotation.id 0
+documenttype[0].datatype[5].sstruct.name "mystruct"
+documenttype[0].datatype[5].sstruct.version 0
+documenttype[0].datatype[5].sstruct.compression.type NONE
+documenttype[0].datatype[5].sstruct.compression.level 0
+documenttype[0].datatype[5].sstruct.compression.threshold 95
+documenttype[0].datatype[5].sstruct.compression.minsize 200
+documenttype[0].datatype[5].sstruct.field[0].name "bytearr"
+documenttype[0].datatype[5].sstruct.field[0].id 1079701754
+documenttype[0].datatype[5].sstruct.field[0].id_v6 1198855694
+documenttype[0].datatype[5].sstruct.field[0].datatype 49942803
+documenttype[0].datatype[5].sstruct.field[1].name "mymap"
+documenttype[0].datatype[5].sstruct.field[1].id 1954178122
+documenttype[0].datatype[5].sstruct.field[1].id_v6 707189723
+documenttype[0].datatype[5].sstruct.field[1].datatype 339965458
+documenttype[0].datatype[5].sstruct.field[2].name "title"
+documenttype[0].datatype[5].sstruct.field[2].id 567626448
+documenttype[0].datatype[5].sstruct.field[2].id_v6 29129762
+documenttype[0].datatype[5].sstruct.field[2].datatype 2
+documenttype[0].datatype[5].sstruct.field[3].name "structfield"
+documenttype[0].datatype[5].sstruct.field[3].id 1726890940
+documenttype[0].datatype[5].sstruct.field[3].id_v6 418303145
+documenttype[0].datatype[5].sstruct.field[3].datatype 2
+documenttype[0].datatype[6].id -1245117006
+documenttype[0].datatype[6].type ARRAY
+documenttype[0].datatype[6].array.element.id 0
+documenttype[0].datatype[6].map.key.id 0
+documenttype[0].datatype[6].map.value.id 0
+documenttype[0].datatype[6].wset.key.id 0
+documenttype[0].datatype[6].wset.createifnonexistent false
+documenttype[0].datatype[6].wset.removeifzero false
+documenttype[0].datatype[6].annotationref.annotation.id 0
+documenttype[0].datatype[6].sstruct.name ""
+documenttype[0].datatype[6].sstruct.version 0
+documenttype[0].datatype[6].sstruct.compression.type NONE
+documenttype[0].datatype[6].sstruct.compression.level 0
+documenttype[0].datatype[6].sstruct.compression.threshold 95
+documenttype[0].datatype[6].sstruct.compression.minsize 200
+documenttype[0].datatype[7].id 1328286588
+documenttype[0].datatype[7].type WSET
+documenttype[0].datatype[7].array.element.id 0
+documenttype[0].datatype[7].map.key.id 0
+documenttype[0].datatype[7].map.value.id 0
+documenttype[0].datatype[7].wset.key.id 2
+documenttype[0].datatype[7].wset.createifnonexistent false
+documenttype[0].datatype[7].wset.removeifzero false
+documenttype[0].datatype[7].annotationref.annotation.id 0
+documenttype[0].datatype[7].sstruct.name ""
+documenttype[0].datatype[7].sstruct.version 0
+documenttype[0].datatype[7].sstruct.compression.type NONE
+documenttype[0].datatype[7].sstruct.compression.level 0
+documenttype[0].datatype[7].sstruct.compression.threshold 95
+documenttype[0].datatype[7].sstruct.compression.minsize 200
+documenttype[0].datatype[8].id 2125328771
+documenttype[0].datatype[8].type WSET
+documenttype[0].datatype[8].array.element.id 0
+documenttype[0].datatype[8].map.key.id 0
+documenttype[0].datatype[8].map.value.id 0
+documenttype[0].datatype[8].wset.key.id 2
+documenttype[0].datatype[8].wset.createifnonexistent false
+documenttype[0].datatype[8].wset.removeifzero true
+documenttype[0].datatype[8].annotationref.annotation.id 0
+documenttype[0].datatype[8].sstruct.name ""
+documenttype[0].datatype[8].sstruct.version 0
+documenttype[0].datatype[8].sstruct.compression.type NONE
+documenttype[0].datatype[8].sstruct.compression.level 0
+documenttype[0].datatype[8].sstruct.compression.threshold 95
+documenttype[0].datatype[8].sstruct.compression.minsize 200
+documenttype[0].datatype[9].id 2065577986
+documenttype[0].datatype[9].type WSET
+documenttype[0].datatype[9].array.element.id 0
+documenttype[0].datatype[9].map.key.id 0
+documenttype[0].datatype[9].map.value.id 0
+documenttype[0].datatype[9].wset.key.id 2
+documenttype[0].datatype[9].wset.createifnonexistent true
+documenttype[0].datatype[9].wset.removeifzero false
+documenttype[0].datatype[9].annotationref.annotation.id 0
+documenttype[0].datatype[9].sstruct.name ""
+documenttype[0].datatype[9].sstruct.version 0
+documenttype[0].datatype[9].sstruct.compression.type NONE
+documenttype[0].datatype[9].sstruct.compression.level 0
+documenttype[0].datatype[9].sstruct.compression.threshold 95
+documenttype[0].datatype[9].sstruct.compression.minsize 200
+documenttype[0].datatype[10].id -1244829667
+documenttype[0].datatype[10].type ARRAY
+documenttype[0].datatype[10].array.element.id 109267174
+documenttype[0].datatype[10].map.key.id 0
+documenttype[0].datatype[10].map.value.id 0
+documenttype[0].datatype[10].wset.key.id 0
+documenttype[0].datatype[10].wset.createifnonexistent false
+documenttype[0].datatype[10].wset.removeifzero false
+documenttype[0].datatype[10].annotationref.annotation.id 0
+documenttype[0].datatype[10].sstruct.name ""
+documenttype[0].datatype[10].sstruct.version 0
+documenttype[0].datatype[10].sstruct.compression.type NONE
+documenttype[0].datatype[10].sstruct.compression.level 0
+documenttype[0].datatype[10].sstruct.compression.threshold 95
+documenttype[0].datatype[10].sstruct.compression.minsize 200
+documenttype[0].datatype[11].id -1584287606
+documenttype[0].datatype[11].type MAP
+documenttype[0].datatype[11].array.element.id 0
+documenttype[0].datatype[11].map.key.id 2
+documenttype[0].datatype[11].map.value.id 0
+documenttype[0].datatype[11].wset.key.id 0
+documenttype[0].datatype[11].wset.createifnonexistent false
+documenttype[0].datatype[11].wset.removeifzero false
+documenttype[0].datatype[11].annotationref.annotation.id 0
+documenttype[0].datatype[11].sstruct.name ""
+documenttype[0].datatype[11].sstruct.version 0
+documenttype[0].datatype[11].sstruct.compression.type NONE
+documenttype[0].datatype[11].sstruct.compression.level 0
+documenttype[0].datatype[11].sstruct.compression.threshold 95
+documenttype[0].datatype[11].sstruct.compression.minsize 200
+documenttype[0].datatype[12].id 2125154557
+documenttype[0].datatype[12].type MAP
+documenttype[0].datatype[12].array.element.id 0
+documenttype[0].datatype[12].map.key.id 2
+documenttype[0].datatype[12].map.value.id 1
+documenttype[0].datatype[12].wset.key.id 0
+documenttype[0].datatype[12].wset.createifnonexistent false
+documenttype[0].datatype[12].wset.removeifzero false
+documenttype[0].datatype[12].annotationref.annotation.id 0
+documenttype[0].datatype[12].sstruct.name ""
+documenttype[0].datatype[12].sstruct.version 0
+documenttype[0].datatype[12].sstruct.compression.type NONE
+documenttype[0].datatype[12].sstruct.compression.level 0
+documenttype[0].datatype[12].sstruct.compression.threshold 95
+documenttype[0].datatype[12].sstruct.compression.minsize 200
+documenttype[0].datatype[13].id -1715531035
+documenttype[0].datatype[13].type MAP
+documenttype[0].datatype[13].array.element.id 0
+documenttype[0].datatype[13].map.key.id 0
+documenttype[0].datatype[13].map.value.id 4
+documenttype[0].datatype[13].wset.key.id 0
+documenttype[0].datatype[13].wset.createifnonexistent false
+documenttype[0].datatype[13].wset.removeifzero false
+documenttype[0].datatype[13].annotationref.annotation.id 0
+documenttype[0].datatype[13].sstruct.name ""
+documenttype[0].datatype[13].sstruct.version 0
+documenttype[0].datatype[13].sstruct.compression.type NONE
+documenttype[0].datatype[13].sstruct.compression.level 0
+documenttype[0].datatype[13].sstruct.compression.threshold 95
+documenttype[0].datatype[13].sstruct.compression.minsize 200
+documenttype[0].datatype[14].id 2138385264
+documenttype[0].datatype[14].type MAP
+documenttype[0].datatype[14].array.element.id 0
+documenttype[0].datatype[14].map.key.id 0
+documenttype[0].datatype[14].map.value.id 5
+documenttype[0].datatype[14].wset.key.id 0
+documenttype[0].datatype[14].wset.createifnonexistent false
+documenttype[0].datatype[14].wset.removeifzero false
+documenttype[0].datatype[14].annotationref.annotation.id 0
+documenttype[0].datatype[14].sstruct.name ""
+documenttype[0].datatype[14].sstruct.version 0
+documenttype[0].datatype[14].sstruct.compression.type NONE
+documenttype[0].datatype[14].sstruct.compression.level 0
+documenttype[0].datatype[14].sstruct.compression.threshold 95
+documenttype[0].datatype[14].sstruct.compression.minsize 200
+documenttype[0].datatype[15].id 435886609
+documenttype[0].datatype[15].type MAP
+documenttype[0].datatype[15].array.element.id 0
+documenttype[0].datatype[15].map.key.id 2
+documenttype[0].datatype[15].map.value.id -1245117006
+documenttype[0].datatype[15].wset.key.id 0
+documenttype[0].datatype[15].wset.createifnonexistent false
+documenttype[0].datatype[15].wset.removeifzero false
+documenttype[0].datatype[15].annotationref.annotation.id 0
+documenttype[0].datatype[15].sstruct.name ""
+documenttype[0].datatype[15].sstruct.version 0
+documenttype[0].datatype[15].sstruct.compression.type NONE
+documenttype[0].datatype[15].sstruct.compression.level 0
+documenttype[0].datatype[15].sstruct.compression.threshold 95
+documenttype[0].datatype[15].sstruct.compression.minsize 200
+documenttype[0].datatype[16].id -1486737430
+documenttype[0].datatype[16].type ARRAY
+documenttype[0].datatype[16].array.element.id 2
+documenttype[0].datatype[16].map.key.id 0
+documenttype[0].datatype[16].map.value.id 0
+documenttype[0].datatype[16].wset.key.id 0
+documenttype[0].datatype[16].wset.createifnonexistent false
+documenttype[0].datatype[16].wset.removeifzero false
+documenttype[0].datatype[16].annotationref.annotation.id 0
+documenttype[0].datatype[16].sstruct.name ""
+documenttype[0].datatype[16].sstruct.version 0
+documenttype[0].datatype[16].sstruct.compression.type NONE
+documenttype[0].datatype[16].sstruct.compression.level 0
+documenttype[0].datatype[16].sstruct.compression.threshold 95
+documenttype[0].datatype[16].sstruct.compression.minsize 200
+documenttype[0].datatype[17].id 1707615575
+documenttype[0].datatype[17].type ARRAY
+documenttype[0].datatype[17].array.element.id -1486737430
+documenttype[0].datatype[17].map.key.id 0
+documenttype[0].datatype[17].map.value.id 0
+documenttype[0].datatype[17].wset.key.id 0
+documenttype[0].datatype[17].wset.createifnonexistent false
+documenttype[0].datatype[17].wset.removeifzero false
+documenttype[0].datatype[17].annotationref.annotation.id 0
+documenttype[0].datatype[17].sstruct.name ""
+documenttype[0].datatype[17].sstruct.version 0
+documenttype[0].datatype[17].sstruct.compression.type NONE
+documenttype[0].datatype[17].sstruct.compression.level 0
+documenttype[0].datatype[17].sstruct.compression.threshold 95
+documenttype[0].datatype[17].sstruct.compression.minsize 200
+documenttype[0].datatype[18].id -794985308
+documenttype[0].datatype[18].type ARRAY
+documenttype[0].datatype[18].array.element.id 1707615575
+documenttype[0].datatype[18].map.key.id 0
+documenttype[0].datatype[18].map.value.id 0
+documenttype[0].datatype[18].wset.key.id 0
+documenttype[0].datatype[18].wset.createifnonexistent false
+documenttype[0].datatype[18].wset.removeifzero false
+documenttype[0].datatype[18].annotationref.annotation.id 0
+documenttype[0].datatype[18].sstruct.name ""
+documenttype[0].datatype[18].sstruct.version 0
+documenttype[0].datatype[18].sstruct.compression.type NONE
+documenttype[0].datatype[18].sstruct.compression.level 0
+documenttype[0].datatype[18].sstruct.compression.threshold 95
+documenttype[0].datatype[18].sstruct.compression.minsize 200
+documenttype[0].datatype[19].id 69621385
+documenttype[0].datatype[19].type ARRAY
+documenttype[0].datatype[19].array.element.id 339965458
+documenttype[0].datatype[19].map.key.id 0
+documenttype[0].datatype[19].map.value.id 0
+documenttype[0].datatype[19].wset.key.id 0
+documenttype[0].datatype[19].wset.createifnonexistent false
+documenttype[0].datatype[19].wset.removeifzero false
+documenttype[0].datatype[19].annotationref.annotation.id 0
+documenttype[0].datatype[19].sstruct.name ""
+documenttype[0].datatype[19].sstruct.version 0
+documenttype[0].datatype[19].sstruct.compression.type NONE
+documenttype[0].datatype[19].sstruct.compression.level 0
+documenttype[0].datatype[19].sstruct.compression.threshold 95
+documenttype[0].datatype[19].sstruct.compression.minsize 200
+documenttype[0].datatype[20].id 1901258752
+documenttype[0].datatype[20].type MAP
+documenttype[0].datatype[20].array.element.id 0
+documenttype[0].datatype[20].map.key.id 0
+documenttype[0].datatype[20].map.value.id -2092985853
+documenttype[0].datatype[20].wset.key.id 0
+documenttype[0].datatype[20].wset.createifnonexistent false
+documenttype[0].datatype[20].wset.removeifzero false
+documenttype[0].datatype[20].annotationref.annotation.id 0
+documenttype[0].datatype[20].sstruct.name ""
+documenttype[0].datatype[20].sstruct.version 0
+documenttype[0].datatype[20].sstruct.compression.type NONE
+documenttype[0].datatype[20].sstruct.compression.level 0
+documenttype[0].datatype[20].sstruct.compression.threshold 95
+documenttype[0].datatype[20].sstruct.compression.minsize 200
+documenttype[0].datatype[21].id 759956026
+documenttype[0].datatype[21].type ARRAY
+documenttype[0].datatype[21].array.element.id -2092985853
+documenttype[0].datatype[21].map.key.id 0
+documenttype[0].datatype[21].map.value.id 0
+documenttype[0].datatype[21].wset.key.id 0
+documenttype[0].datatype[21].wset.createifnonexistent false
+documenttype[0].datatype[21].wset.removeifzero false
+documenttype[0].datatype[21].annotationref.annotation.id 0
+documenttype[0].datatype[21].sstruct.name ""
+documenttype[0].datatype[21].sstruct.version 0
+documenttype[0].datatype[21].sstruct.compression.type NONE
+documenttype[0].datatype[21].sstruct.compression.level 0
+documenttype[0].datatype[21].sstruct.compression.threshold 95
+documenttype[0].datatype[21].sstruct.compression.minsize 200
+documenttype[0].datatype[22].id -389833101
+documenttype[0].datatype[22].type MAP
+documenttype[0].datatype[22].array.element.id 0
+documenttype[0].datatype[22].map.key.id 0
+documenttype[0].datatype[22].map.value.id 294108848
+documenttype[0].datatype[22].wset.key.id 0
+documenttype[0].datatype[22].wset.createifnonexistent false
+documenttype[0].datatype[22].wset.removeifzero false
+documenttype[0].datatype[22].annotationref.annotation.id 0
+documenttype[0].datatype[22].sstruct.name ""
+documenttype[0].datatype[22].sstruct.version 0
+documenttype[0].datatype[22].sstruct.compression.type NONE
+documenttype[0].datatype[22].sstruct.compression.level 0
+documenttype[0].datatype[22].sstruct.compression.threshold 95
+documenttype[0].datatype[22].sstruct.compression.minsize 200
+documenttype[0].datatype[23].id 1328581348
+documenttype[0].datatype[23].type STRUCT
+documenttype[0].datatype[23].array.element.id 0
+documenttype[0].datatype[23].map.key.id 0
+documenttype[0].datatype[23].map.value.id 0
+documenttype[0].datatype[23].wset.key.id 0
+documenttype[0].datatype[23].wset.createifnonexistent false
+documenttype[0].datatype[23].wset.removeifzero false
+documenttype[0].datatype[23].annotationref.annotation.id 0
+documenttype[0].datatype[23].sstruct.name "types.header"
+documenttype[0].datatype[23].sstruct.version 0
+documenttype[0].datatype[23].sstruct.compression.type NONE
+documenttype[0].datatype[23].sstruct.compression.level 0
+documenttype[0].datatype[23].sstruct.compression.threshold 95
+documenttype[0].datatype[23].sstruct.compression.minsize 200
+documenttype[0].datatype[23].sstruct.field[0].name "abyte"
+documenttype[0].datatype[23].sstruct.field[0].id 110138156
+documenttype[0].datatype[23].sstruct.field[0].id_v6 1369099343
+documenttype[0].datatype[23].sstruct.field[0].datatype 16
+documenttype[0].datatype[23].sstruct.field[1].name "along"
+documenttype[0].datatype[23].sstruct.field[1].id 1206464520
+documenttype[0].datatype[23].sstruct.field[1].id_v6 871280609
+documenttype[0].datatype[23].sstruct.field[1].datatype 4
+documenttype[0].datatype[23].sstruct.field[2].name "arrayfield"
+documenttype[0].datatype[23].sstruct.field[2].id 965790107
+documenttype[0].datatype[23].sstruct.field[2].id_v6 1010955705
+documenttype[0].datatype[23].sstruct.field[2].datatype -1245117006
+documenttype[0].datatype[23].sstruct.field[3].name "setfield"
+documenttype[0].datatype[23].sstruct.field[3].id 761581914
+documenttype[0].datatype[23].sstruct.field[3].id_v6 1762943268
+documenttype[0].datatype[23].sstruct.field[3].datatype 1328286588
+documenttype[0].datatype[23].sstruct.field[4].name "pos"
+documenttype[0].datatype[23].sstruct.field[4].id 1041567475
+documenttype[0].datatype[23].sstruct.field[4].id_v6 26353693
+documenttype[0].datatype[23].sstruct.field[4].datatype 1381038251
+documenttype[0].datatype[23].sstruct.field[5].name "setfield2"
+documenttype[0].datatype[23].sstruct.field[5].id 1066659198
+documenttype[0].datatype[23].sstruct.field[5].id_v6 813038565
+documenttype[0].datatype[23].sstruct.field[5].datatype 18
+documenttype[0].datatype[23].sstruct.field[6].name "setfield3"
+documenttype[0].datatype[23].sstruct.field[6].id 1180155772
+documenttype[0].datatype[23].sstruct.field[6].id_v6 1697232199
+documenttype[0].datatype[23].sstruct.field[6].datatype 2125328771
+documenttype[0].datatype[23].sstruct.field[7].name "setfield4"
+documenttype[0].datatype[23].sstruct.field[7].id 1254131631
+documenttype[0].datatype[23].sstruct.field[7].id_v6 119755202
+documenttype[0].datatype[23].sstruct.field[7].datatype 2065577986
+documenttype[0].datatype[23].sstruct.field[8].name "tagfield"
+documenttype[0].datatype[23].sstruct.field[8].id 1653562069
+documenttype[0].datatype[23].sstruct.field[8].id_v6 938523246
+documenttype[0].datatype[23].sstruct.field[8].datatype 18
+documenttype[0].datatype[23].sstruct.field[9].name "structfield"
+documenttype[0].datatype[23].sstruct.field[9].id 486207386
+documenttype[0].datatype[23].sstruct.field[9].id_v6 418303145
+documenttype[0].datatype[23].sstruct.field[9].datatype 109267174
+documenttype[0].datatype[23].sstruct.field[10].name "structarrayfield"
+documenttype[0].datatype[23].sstruct.field[10].id 335048518
+documenttype[0].datatype[23].sstruct.field[10].id_v6 607034174
+documenttype[0].datatype[23].sstruct.field[10].datatype -1244829667
+documenttype[0].datatype[23].sstruct.field[11].name "stringmapfield"
+documenttype[0].datatype[23].sstruct.field[11].id 117465687
+documenttype[0].datatype[23].sstruct.field[11].id_v6 1492788095
+documenttype[0].datatype[23].sstruct.field[11].datatype 339965458
+documenttype[0].datatype[23].sstruct.field[12].name "intmapfield"
+documenttype[0].datatype[23].sstruct.field[12].id 121004462
+documenttype[0].datatype[23].sstruct.field[12].id_v6 1642487905
+documenttype[0].datatype[23].sstruct.field[12].datatype -1584287606
+documenttype[0].datatype[23].sstruct.field[13].name "floatmapfield"
+documenttype[0].datatype[23].sstruct.field[13].id 1239120925
+documenttype[0].datatype[23].sstruct.field[13].id_v6 1609437589
+documenttype[0].datatype[23].sstruct.field[13].datatype 2125154557
+documenttype[0].datatype[23].sstruct.field[14].name "longmapfield"
+documenttype[0].datatype[23].sstruct.field[14].id 477718745
+documenttype[0].datatype[23].sstruct.field[14].id_v6 920341968
+documenttype[0].datatype[23].sstruct.field[14].datatype -1715531035
+documenttype[0].datatype[23].sstruct.field[15].name "doublemapfield"
+documenttype[0].datatype[23].sstruct.field[15].id 877047192
+documenttype[0].datatype[23].sstruct.field[15].id_v6 957317090
+documenttype[0].datatype[23].sstruct.field[15].datatype 2138385264
+documenttype[0].datatype[23].sstruct.field[16].name "arraymapfield"
+documenttype[0].datatype[23].sstruct.field[16].id 1670805928
+documenttype[0].datatype[23].sstruct.field[16].id_v6 1940354311
+documenttype[0].datatype[23].sstruct.field[16].datatype 435886609
+documenttype[0].datatype[23].sstruct.field[17].name "arrarr"
+documenttype[0].datatype[23].sstruct.field[17].id 1962567166
+documenttype[0].datatype[23].sstruct.field[17].id_v6 885141301
+documenttype[0].datatype[23].sstruct.field[17].datatype -794985308
+documenttype[0].datatype[23].sstruct.field[18].name "maparr"
+documenttype[0].datatype[23].sstruct.field[18].id 904375219
+documenttype[0].datatype[23].sstruct.field[18].id_v6 63700074
+documenttype[0].datatype[23].sstruct.field[18].datatype 69621385
+documenttype[0].datatype[23].sstruct.field[19].name "mystructfield"
+documenttype[0].datatype[23].sstruct.field[19].id 1348513378
+documenttype[0].datatype[23].sstruct.field[19].id_v6 2033170300
+documenttype[0].datatype[23].sstruct.field[19].datatype -2092985853
+documenttype[0].datatype[23].sstruct.field[20].name "mystructmap"
+documenttype[0].datatype[23].sstruct.field[20].id 1511423250
+documenttype[0].datatype[23].sstruct.field[20].id_v6 449602635
+documenttype[0].datatype[23].sstruct.field[20].datatype 1901258752
+documenttype[0].datatype[23].sstruct.field[21].name "mystructarr"
+documenttype[0].datatype[23].sstruct.field[21].id 595856991
+documenttype[0].datatype[23].sstruct.field[21].id_v6 764861972
+documenttype[0].datatype[23].sstruct.field[21].datatype 759956026
+documenttype[0].datatype[23].sstruct.field[22].name "Folders"
+documenttype[0].datatype[23].sstruct.field[22].id 34575524
+documenttype[0].datatype[23].sstruct.field[22].id_v6 280569744
+documenttype[0].datatype[23].sstruct.field[22].datatype -389833101
+documenttype[0].datatype[23].sstruct.field[23].name "juletre"
+documenttype[0].datatype[23].sstruct.field[23].id 1039981530
+documenttype[0].datatype[23].sstruct.field[23].id_v6 2073084146
+documenttype[0].datatype[23].sstruct.field[23].datatype 4
+documenttype[0].datatype[23].sstruct.field[24].name "album0"
+documenttype[0].datatype[23].sstruct.field[24].id 764312262
+documenttype[0].datatype[23].sstruct.field[24].id_v6 1409364160
+documenttype[0].datatype[23].sstruct.field[24].datatype 18
+documenttype[0].datatype[23].sstruct.field[25].name "album1"
+documenttype[0].datatype[23].sstruct.field[25].id 1967160809
+documenttype[0].datatype[23].sstruct.field[25].id_v6 1833811264
+documenttype[0].datatype[23].sstruct.field[25].datatype 18
+documenttype[0].datatype[23].sstruct.field[26].name "other"
+documenttype[0].datatype[23].sstruct.field[26].id 2443357
+documenttype[0].datatype[23].sstruct.field[26].id_v6 903806222
+documenttype[0].datatype[23].sstruct.field[26].datatype 4
+documenttype[0].datatype[23].sstruct.field[27].name "rankfeatures"
+documenttype[0].datatype[23].sstruct.field[27].id 1883197392
+documenttype[0].datatype[23].sstruct.field[27].id_v6 699950698
+documenttype[0].datatype[23].sstruct.field[27].datatype 2
+documenttype[0].datatype[23].sstruct.field[28].name "summaryfeatures"
+documenttype[0].datatype[23].sstruct.field[28].id 1840337115
+documenttype[0].datatype[23].sstruct.field[28].id_v6 1981648971
+documenttype[0].datatype[23].sstruct.field[28].datatype 2
+documenttype[0].datatype[24].id 171503364
+documenttype[0].datatype[24].type MAP
+documenttype[0].datatype[24].array.element.id 0
+documenttype[0].datatype[24].map.key.id 1707615575
+documenttype[0].datatype[24].map.value.id 0
+documenttype[0].datatype[24].wset.key.id 0
+documenttype[0].datatype[24].wset.createifnonexistent false
+documenttype[0].datatype[24].wset.removeifzero false
+documenttype[0].datatype[24].annotationref.annotation.id 0
+documenttype[0].datatype[24].sstruct.name ""
+documenttype[0].datatype[24].sstruct.version 0
+documenttype[0].datatype[24].sstruct.compression.type NONE
+documenttype[0].datatype[24].sstruct.compression.level 0
+documenttype[0].datatype[24].sstruct.compression.threshold 95
+documenttype[0].datatype[24].sstruct.compression.minsize 200
+documenttype[0].datatype[25].id 1100964733
+documenttype[0].datatype[25].type ARRAY
+documenttype[0].datatype[25].array.element.id 171503364
+documenttype[0].datatype[25].map.key.id 0
+documenttype[0].datatype[25].map.value.id 0
+documenttype[0].datatype[25].wset.key.id 0
+documenttype[0].datatype[25].wset.createifnonexistent false
+documenttype[0].datatype[25].wset.removeifzero false
+documenttype[0].datatype[25].annotationref.annotation.id 0
+documenttype[0].datatype[25].sstruct.name ""
+documenttype[0].datatype[25].sstruct.version 0
+documenttype[0].datatype[25].sstruct.compression.type NONE
+documenttype[0].datatype[25].sstruct.compression.level 0
+documenttype[0].datatype[25].sstruct.compression.threshold 95
+documenttype[0].datatype[25].sstruct.compression.minsize 200
+documenttype[0].datatype[26].id 348447225
+documenttype[0].datatype[26].type STRUCT
+documenttype[0].datatype[26].array.element.id 0
+documenttype[0].datatype[26].map.key.id 0
+documenttype[0].datatype[26].map.value.id 0
+documenttype[0].datatype[26].wset.key.id 0
+documenttype[0].datatype[26].wset.createifnonexistent false
+documenttype[0].datatype[26].wset.removeifzero false
+documenttype[0].datatype[26].annotationref.annotation.id 0
+documenttype[0].datatype[26].sstruct.name "types.body"
+documenttype[0].datatype[26].sstruct.version 0
+documenttype[0].datatype[26].sstruct.compression.type NONE
+documenttype[0].datatype[26].sstruct.compression.level 0
+documenttype[0].datatype[26].sstruct.compression.threshold 95
+documenttype[0].datatype[26].sstruct.compression.minsize 200
+documenttype[0].datatype[26].sstruct.field[0].name "complexarray"
+documenttype[0].datatype[26].sstruct.field[0].id 1028383787
+documenttype[0].datatype[26].sstruct.field[0].id_v6 658530305
+documenttype[0].datatype[26].sstruct.field[0].datatype 1100964733
+documenttype[0].fieldsets{[document]}.fields[0] "Folders"
+documenttype[0].fieldsets{[document]}.fields[1] "abyte"
+documenttype[0].fieldsets{[document]}.fields[2] "album0"
+documenttype[0].fieldsets{[document]}.fields[3] "album1"
+documenttype[0].fieldsets{[document]}.fields[4] "along"
+documenttype[0].fieldsets{[document]}.fields[5] "arrarr"
+documenttype[0].fieldsets{[document]}.fields[6] "arrayfield"
+documenttype[0].fieldsets{[document]}.fields[7] "arraymapfield"
+documenttype[0].fieldsets{[document]}.fields[8] "complexarray"
+documenttype[0].fieldsets{[document]}.fields[9] "doublemapfield"
+documenttype[0].fieldsets{[document]}.fields[10] "floatmapfield"
+documenttype[0].fieldsets{[document]}.fields[11] "intmapfield"
+documenttype[0].fieldsets{[document]}.fields[12] "juletre"
+documenttype[0].fieldsets{[document]}.fields[13] "longmapfield"
+documenttype[0].fieldsets{[document]}.fields[14] "maparr"
+documenttype[0].fieldsets{[document]}.fields[15] "mystructarr"
+documenttype[0].fieldsets{[document]}.fields[16] "mystructfield"
+documenttype[0].fieldsets{[document]}.fields[17] "mystructmap"
+documenttype[0].fieldsets{[document]}.fields[18] "pos"
+documenttype[0].fieldsets{[document]}.fields[19] "setfield"
+documenttype[0].fieldsets{[document]}.fields[20] "setfield2"
+documenttype[0].fieldsets{[document]}.fields[21] "setfield3"
+documenttype[0].fieldsets{[document]}.fields[22] "setfield4"
+documenttype[0].fieldsets{[document]}.fields[23] "stringmapfield"
+documenttype[0].fieldsets{[document]}.fields[24] "structarrayfield"
+documenttype[0].fieldsets{[document]}.fields[25] "structfield"
+documenttype[0].fieldsets{[document]}.fields[26] "tagfield"
diff --git a/config-model/src/test/configmodel/types/documenttypes_with_doc_field.cfg b/config-model/src/test/configmodel/types/documenttypes_with_doc_field.cfg
new file mode 100644
index 00000000000..d13195f1ffe
--- /dev/null
+++ b/config-model/src/test/configmodel/types/documenttypes_with_doc_field.cfg
@@ -0,0 +1,109 @@
+enablecompression false
+documenttype[0].id -1368624373
+documenttype[0].name "other_doc"
+documenttype[0].version 0
+documenttype[0].headerstruct 1631005140
+documenttype[0].bodystruct 549879017
+documenttype[0].inherits[0].id 8
+documenttype[0].datatype[0].id 1631005140
+documenttype[0].datatype[0].type STRUCT
+documenttype[0].datatype[0].array.element.id 0
+documenttype[0].datatype[0].map.key.id 0
+documenttype[0].datatype[0].map.value.id 0
+documenttype[0].datatype[0].wset.key.id 0
+documenttype[0].datatype[0].wset.createifnonexistent false
+documenttype[0].datatype[0].wset.removeifzero false
+documenttype[0].datatype[0].annotationref.annotation.id 0
+documenttype[0].datatype[0].sstruct.name "other_doc.header"
+documenttype[0].datatype[0].sstruct.version 0
+documenttype[0].datatype[0].sstruct.compression.type NONE
+documenttype[0].datatype[0].sstruct.compression.level 0
+documenttype[0].datatype[0].sstruct.compression.threshold 95
+documenttype[0].datatype[0].sstruct.compression.minsize 200
+documenttype[0].datatype[0].sstruct.field[0].name "rankfeatures"
+documenttype[0].datatype[0].sstruct.field[0].id 1883197392
+documenttype[0].datatype[0].sstruct.field[0].id_v6 699950698
+documenttype[0].datatype[0].sstruct.field[0].datatype 2
+documenttype[0].datatype[0].sstruct.field[1].name "summaryfeatures"
+documenttype[0].datatype[0].sstruct.field[1].id 1840337115
+documenttype[0].datatype[0].sstruct.field[1].id_v6 1981648971
+documenttype[0].datatype[0].sstruct.field[1].datatype 2
+documenttype[0].datatype[1].id 549879017
+documenttype[0].datatype[1].type STRUCT
+documenttype[0].datatype[1].array.element.id 0
+documenttype[0].datatype[1].map.key.id 0
+documenttype[0].datatype[1].map.value.id 0
+documenttype[0].datatype[1].wset.key.id 0
+documenttype[0].datatype[1].wset.createifnonexistent false
+documenttype[0].datatype[1].wset.removeifzero false
+documenttype[0].datatype[1].annotationref.annotation.id 0
+documenttype[0].datatype[1].sstruct.name "other_doc.body"
+documenttype[0].datatype[1].sstruct.version 0
+documenttype[0].datatype[1].sstruct.compression.type NONE
+documenttype[0].datatype[1].sstruct.compression.level 0
+documenttype[0].datatype[1].sstruct.compression.threshold 95
+documenttype[0].datatype[1].sstruct.compression.minsize 200
+documenttype[1].id -853072901
+documenttype[1].name "types"
+documenttype[1].version 0
+documenttype[1].headerstruct 1328581348
+documenttype[1].bodystruct 348447225
+documenttype[1].inherits[0].id 8
+documenttype[1].datatype[0].id -1368624373
+documenttype[1].datatype[0].type STRUCT
+documenttype[1].datatype[0].array.element.id 0
+documenttype[1].datatype[0].map.key.id 0
+documenttype[1].datatype[0].map.value.id 0
+documenttype[1].datatype[0].wset.key.id 0
+documenttype[1].datatype[0].wset.createifnonexistent false
+documenttype[1].datatype[0].wset.removeifzero false
+documenttype[1].datatype[0].annotationref.annotation.id 0
+documenttype[1].datatype[0].sstruct.name "other_doc"
+documenttype[1].datatype[0].sstruct.version 0
+documenttype[1].datatype[0].sstruct.compression.type NONE
+documenttype[1].datatype[0].sstruct.compression.level 0
+documenttype[1].datatype[0].sstruct.compression.threshold 95
+documenttype[1].datatype[0].sstruct.compression.minsize 200
+documenttype[1].datatype[1].id 1328581348
+documenttype[1].datatype[1].type STRUCT
+documenttype[1].datatype[1].array.element.id 0
+documenttype[1].datatype[1].map.key.id 0
+documenttype[1].datatype[1].map.value.id 0
+documenttype[1].datatype[1].wset.key.id 0
+documenttype[1].datatype[1].wset.createifnonexistent false
+documenttype[1].datatype[1].wset.removeifzero false
+documenttype[1].datatype[1].annotationref.annotation.id 0
+documenttype[1].datatype[1].sstruct.name "types.header"
+documenttype[1].datatype[1].sstruct.version 0
+documenttype[1].datatype[1].sstruct.compression.type NONE
+documenttype[1].datatype[1].sstruct.compression.level 0
+documenttype[1].datatype[1].sstruct.compression.threshold 95
+documenttype[1].datatype[1].sstruct.compression.minsize 200
+documenttype[1].datatype[1].sstruct.field[0].name "doc_field"
+documenttype[1].datatype[1].sstruct.field[0].id 819293364
+documenttype[1].datatype[1].sstruct.field[0].id_v6 1634907905
+documenttype[1].datatype[1].sstruct.field[0].datatype -1368624373
+documenttype[1].datatype[1].sstruct.field[1].name "rankfeatures"
+documenttype[1].datatype[1].sstruct.field[1].id 1883197392
+documenttype[1].datatype[1].sstruct.field[1].id_v6 699950698
+documenttype[1].datatype[1].sstruct.field[1].datatype 2
+documenttype[1].datatype[1].sstruct.field[2].name "summaryfeatures"
+documenttype[1].datatype[1].sstruct.field[2].id 1840337115
+documenttype[1].datatype[1].sstruct.field[2].id_v6 1981648971
+documenttype[1].datatype[1].sstruct.field[2].datatype 2
+documenttype[1].datatype[2].id 348447225
+documenttype[1].datatype[2].type STRUCT
+documenttype[1].datatype[2].array.element.id 0
+documenttype[1].datatype[2].map.key.id 0
+documenttype[1].datatype[2].map.value.id 0
+documenttype[1].datatype[2].wset.key.id 0
+documenttype[1].datatype[2].wset.createifnonexistent false
+documenttype[1].datatype[2].wset.removeifzero false
+documenttype[1].datatype[2].annotationref.annotation.id 0
+documenttype[1].datatype[2].sstruct.name "types.body"
+documenttype[1].datatype[2].sstruct.version 0
+documenttype[1].datatype[2].sstruct.compression.type NONE
+documenttype[1].datatype[2].sstruct.compression.level 0
+documenttype[1].datatype[2].sstruct.compression.threshold 95
+documenttype[1].datatype[2].sstruct.compression.minsize 200
+documenttype[1].fieldsets{[document]}.fields[0] "doc_field"
diff --git a/config-model/src/test/configmodel/types/other_doc.sd b/config-model/src/test/configmodel/types/other_doc.sd
new file mode 100644
index 00000000000..3852270361a
--- /dev/null
+++ b/config-model/src/test/configmodel/types/other_doc.sd
@@ -0,0 +1,3 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document other_doc {
+}
diff --git a/config-model/src/test/configmodel/types/type_with_doc_field.sd b/config-model/src/test/configmodel/types/type_with_doc_field.sd
new file mode 100644
index 00000000000..79aa3495b58
--- /dev/null
+++ b/config-model/src/test/configmodel/types/type_with_doc_field.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search types {
+
+ document types {
+
+ field doc_field type other_doc {
+ }
+
+ }
+}
diff --git a/config-model/src/test/configmodel/types/types.sd b/config-model/src/test/configmodel/types/types.sd
new file mode 100644
index 00000000000..b42ac6a4860
--- /dev/null
+++ b/config-model/src/test/configmodel/types/types.sd
@@ -0,0 +1,151 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search types {
+
+ document types {
+
+ field abyte type byte {
+ indexing: summary | attribute
+ }
+
+ field along type long {
+ indexing: summary | attribute
+ }
+
+ field arrayfield type array<int> {
+ indexing: attribute
+ }
+
+ field setfield type weightedset<string> {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+ field pos type position {
+ }
+
+ field setfield2 type weightedset<string> {
+ indexing: attribute
+ weightedset: remove-if-zero
+ weightedset: create-if-nonexistent
+ rank-type: identity
+ #match {
+ # token
+ #}
+ }
+
+ field setfield3 type weightedset<string> {
+ weightedset: remove-if-zero
+ indexing: attribute
+ rank-type: identity
+ match {
+ token
+ }
+ }
+
+ field setfield4 type weightedset<string> {
+ weightedset: create-if-nonexistent
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field tagfield type tag {
+ indexing: attribute | summary
+ match {
+ token
+ }
+ }
+ struct sct {
+ field s1 type string {}
+ field s2 type string {}
+ }
+ field structfield type sct {
+ }
+ field structarrayfield type array<sct> {
+ }
+ field stringmapfield type map<string, string> {
+ indexing: index | summary
+ }
+ field intmapfield type map<string, int> {
+
+ }
+ field floatmapfield type map<string, float> {
+
+ }
+ field longmapfield type map<int, long> {
+
+ }
+ field doublemapfield type map<int, double> {
+
+ }
+ field arraymapfield type map<string,array<int>> {
+
+ }
+ #field complexfield type map<array<sct>, map<int,array<float>>> {
+ #}
+ #field wildcardfield type map<int,?> {
+ #}
+ #field wildcardfield2 type map<?,?> {
+ #}
+
+ field arrarr type array<array<array<string>>> {header}
+ field maparr type array<map<string, string>> {header}
+ field complexarray type array< map<array<array<string>>, int> > {body}
+
+ struct mystruct {
+ field bytearr type array<byte>{}
+ field mymap type map<string, string>{}
+ field title type string {}
+ field structfield type string {}
+ }
+
+ field mystructfield type mystruct {header}
+ field mystructmap type map<int, mystruct> {header}
+ field mystructarr type array<mystruct> {header}
+
+ struct folder {
+ field Version type int {}
+ field Name type string {}
+ field FlagsCounter type map<string,long> {}
+ field anotherfolder type folder {}
+ }
+
+
+ field Folders type map<int,folder> {}
+
+ field juletre type long {
+ indexing: attribute
+ attribute {
+ fast-search
+ }
+ }
+
+ # Field defined same way as tag
+ field album0 type weightedset<string> {
+ indexing: summary
+ # This is pointless, but
+ weightedset {
+ create-if-nonexistent
+ remove-if-zero
+ }
+ header
+ }
+
+ # Field defined same way as tag
+ field album1 type weightedset<string> {
+ indexing: attribute | summary
+ weightedset {
+ create-if-nonexistent
+ remove-if-zero
+ }
+ header
+ }
+
+ }
+
+ field other type long {
+ indexing: input along | attribute
+ }
+}
diff --git a/config-model/src/test/derived/advanced/advanced.sd b/config-model/src/test/derived/advanced/advanced.sd
new file mode 100644
index 00000000000..7d6623e268f
--- /dev/null
+++ b/config-model/src/test/derived/advanced/advanced.sd
@@ -0,0 +1,105 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search advanced {
+ document advanced {
+ field debug_src type string { }
+ field attributes_src type string { }
+ field location_str type string { }
+ field title_src type string { }
+ field product_src type string { }
+ field product2_src type string { }
+ field product3_src type string { }
+ }
+ field debug type string {
+ indexing {
+
+ # Initialize variables used for superduper ranking
+ 0 | set_var superduperus | set_var superdupereu | set_var superduperasia;
+
+ input debug_src | lowercase | summary | index | split ";" | for_each {
+ # Loop through each token in debug string
+ switch {
+ case "superduperus": 10 | set_var superduperus;
+ case "superdupereu": 10 | set_var superdupereu;
+ case "superduperasia": 10 | set_var superduperasia;
+ }
+ };
+ }
+ stemming: none
+ }
+
+ field attributes type string {
+ indexing {
+
+ # Initialize variables used for superduper ranking
+ 1 | set_var superdupermod;
+ 2 | set_var tmppubdate;
+ input attributes_src | lowercase | summary | index | split ";" | for_each {
+ # Loop through each token in attributes string
+ switch {
+
+ # De-rank PR articles using the following rules:
+ # 1. Set editedstaticrank to '1'
+ # 2. Subtract 2.5 hours (9000 seconds) from timestamp used in ranking
+ # 3. No superduper rank
+ case "typepr": 1 | set_var tmpsourcerank | get_var tmppubdate - 9000 | set_var tmppubdate | 0 | set_var superdupermod;
+ }
+ };
+ }
+ stemming: none
+ }
+
+ field title type string {
+ indexing: input title_src | index
+ indexing-rewrite: none
+ rank-type: identity
+ stemming: none
+ alias: headline
+ }
+
+ field title_s type string {
+ indexing: input title_src | summary
+ }
+
+ field product type string {
+ indexing: input product_src | switch {
+ case "": "myweb" | index | summary;
+ default: input product_src | index | summary;
+ }
+ indexing-rewrite: none
+ normalizing: none
+ }
+
+ field product2 type string {
+ indexing {
+ if (input product2_src == "foo") {
+ "bar" | index product2;
+ } else {
+ "baz" | index product2;
+ };
+ }
+ indexing-rewrite: none
+ normalizing: none
+ }
+
+ field product3 type string {
+ indexing: input product3_src | switch {
+ case "": "myweb" | index product3 | summary product3;
+ default: input product3_src | index product3 | summary product3;
+ }
+ normalizing: none
+ indexing-rewrite: none
+ }
+
+ field location type position {
+ indexing: input location_str | to_pos | attribute
+ }
+ field mysummary type string {
+ indexing: input title_src | summary
+ }
+ fieldset titleabstract {
+ fields: title
+ }
+ fieldset default {
+ fields: title
+ }
+}
diff --git a/config-model/src/test/derived/advanced/attributes.cfg b/config-model/src/test/derived/advanced/attributes.cfg
new file mode 100644
index 00000000000..afc25d5975c
--- /dev/null
+++ b/config-model/src/test/derived/advanced/attributes.cfg
@@ -0,0 +1,19 @@
+attribute[0].name "location_zcurve"
+attribute[0].datatype INT64
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch true
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/advanced/documentmanager.cfg b/config-model/src/test/derived/advanced/documentmanager.cfg
new file mode 100644
index 00000000000..98c3d379987
--- /dev/null
+++ b/config-model/src/test/derived/advanced/documentmanager.cfg
@@ -0,0 +1,84 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -1486737430
+datatype[1].arraytype[0].datatype 2
+datatype[2].id -1337915045
+datatype[2].structtype[0].name "advanced.header"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "debug_src"
+datatype[2].structtype[0].field[0].datatype 2
+datatype[2].structtype[0].field[1].name "attributes_src"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[2].structtype[0].field[2].name "location_str"
+datatype[2].structtype[0].field[2].datatype 2
+datatype[2].structtype[0].field[3].name "title_src"
+datatype[2].structtype[0].field[3].datatype 2
+datatype[2].structtype[0].field[4].name "product_src"
+datatype[2].structtype[0].field[4].datatype 2
+datatype[2].structtype[0].field[5].name "product2_src"
+datatype[2].structtype[0].field[5].datatype 2
+datatype[2].structtype[0].field[6].name "product3_src"
+datatype[2].structtype[0].field[6].datatype 2
+datatype[2].structtype[0].field[7].name "debug"
+datatype[2].structtype[0].field[7].datatype 2
+datatype[2].structtype[0].field[8].name "attributes"
+datatype[2].structtype[0].field[8].datatype 2
+datatype[2].structtype[0].field[9].name "title"
+datatype[2].structtype[0].field[9].datatype 2
+datatype[2].structtype[0].field[10].name "product"
+datatype[2].structtype[0].field[10].datatype 2
+datatype[2].structtype[0].field[11].name "product2"
+datatype[2].structtype[0].field[11].datatype 2
+datatype[2].structtype[0].field[12].name "product3"
+datatype[2].structtype[0].field[12].datatype 2
+datatype[2].structtype[0].field[13].name "location_zcurve"
+datatype[2].structtype[0].field[13].datatype 4
+datatype[2].structtype[0].field[14].name "title_s"
+datatype[2].structtype[0].field[14].datatype 2
+datatype[2].structtype[0].field[15].name "location.position"
+datatype[2].structtype[0].field[15].datatype -1486737430
+datatype[2].structtype[0].field[16].name "location.distance"
+datatype[2].structtype[0].field[16].datatype 0
+datatype[2].structtype[0].field[17].name "mysummary"
+datatype[2].structtype[0].field[17].datatype 2
+datatype[2].structtype[0].field[18].name "rankfeatures"
+datatype[2].structtype[0].field[18].datatype 2
+datatype[2].structtype[0].field[19].name "summaryfeatures"
+datatype[2].structtype[0].field[19].datatype 2
+datatype[3].id -704605648
+datatype[3].structtype[0].name "advanced.body"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[4].id 686681444
+datatype[4].documenttype[0].name "advanced"
+datatype[4].documenttype[0].version 0
+datatype[4].documenttype[0].inherits[0].name "document"
+datatype[4].documenttype[0].inherits[0].version 0
+datatype[4].documenttype[0].headerstruct -1337915045
+datatype[4].documenttype[0].bodystruct -704605648
+datatype[4].documenttype[0].fieldsets{titleabstract}.fields[0] "title"
+datatype[4].documenttype[0].fieldsets{default}.fields[0] "title"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[0] "attributes_src"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[1] "debug_src"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[2] "location_str"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[3] "product2_src"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[4] "product3_src"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[5] "product_src"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[6] "title_src"
diff --git a/config-model/src/test/derived/advanced/ilscripts.cfg b/config-model/src/test/derived/advanced/ilscripts.cfg
new file mode 100644
index 00000000000..ffa9a7e060f
--- /dev/null
+++ b/config-model/src/test/derived/advanced/ilscripts.cfg
@@ -0,0 +1,24 @@
+maxtermoccurrences 100
+ilscript[0].doctype "advanced"
+ilscript[0].docfield[0] "debug_src"
+ilscript[0].docfield[1] "attributes_src"
+ilscript[0].docfield[2] "location_str"
+ilscript[0].docfield[3] "title_src"
+ilscript[0].docfield[4] "product_src"
+ilscript[0].docfield[5] "product2_src"
+ilscript[0].docfield[6] "product3_src"
+ilscript[0].content[0] "clear_state | guard { 1 | set_var superdupermod; 2 | set_var tmppubdate; input attributes_src | lowercase | tokenize normalize | summary attributes | index attributes | split \";\" | for_each { switch { case \"typepr\": 1 | set_var tmpsourcerank | get_var tmppubdate - 9000 | set_var tmppubdate | 0 | set_var superdupermod; } }; }"
+ilscript[0].content[1] "clear_state | guard { 0 | set_var superduperus | set_var superdupereu | set_var superduperasia; input debug_src | lowercase | tokenize normalize | summary debug | index debug | split \";\" | for_each { switch { case \"superduperus\": 10 | set_var superduperus; case \"superdupereu\": 10 | set_var superdupereu; case \"superduperasia\": 10 | set_var superduperasia; } }; }"
+ilscript[0].content[2] "clear_state | guard { input location_str | to_pos | zcurve | attribute location_zcurve; }"
+ilscript[0].content[3] "clear_state | guard { input title_src | summary mysummary; }"
+ilscript[0].content[4] "clear_state | guard { input product_src | switch { case \"\": \"myweb\" | tokenize stem:\"SHORTEST\" | index product | summary product; default: input product_src | tokenize stem:\"SHORTEST\" | index product | summary product; }; }"
+ilscript[0].content[5] "clear_state | guard { if (input product2_src == \"foo\") { \"bar\" | tokenize stem:\"SHORTEST\" | index product2; } else { \"baz\" | tokenize stem:\"SHORTEST\" | index product2; }; }"
+ilscript[0].content[6] "clear_state | guard { input product3_src | switch { case \"\": \"myweb\" | tokenize stem:\"SHORTEST\" | index product3 | summary product3; default: input product3_src | tokenize stem:\"SHORTEST\" | index product3 | summary product3; }; }"
+ilscript[0].content[7] "clear_state | guard { input title_src | tokenize normalize | index title; }"
+ilscript[0].content[8] "clear_state | guard { input title_src | summary title_s; }"
+ilscript[0].content[9] "input attributes_src | passthrough attributes_src"
+ilscript[0].content[10] "input debug_src | passthrough debug_src"
+ilscript[0].content[11] "input product2_src | passthrough product2_src"
+ilscript[0].content[12] "input product3_src | passthrough product3_src"
+ilscript[0].content[13] "input product_src | passthrough product_src"
+ilscript[0].content[14] "input title_src | passthrough title_src" \ No newline at end of file
diff --git a/config-model/src/test/derived/advanced/index-info.cfg b/config-model/src/test/derived/advanced/index-info.cfg
new file mode 100644
index 00000000000..1bb68110dae
--- /dev/null
+++ b/config-model/src/test/derived/advanced/index-info.cfg
@@ -0,0 +1,97 @@
+indexinfo[0].name "advanced"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "debug_src"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "attributes_src"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "location_str"
+indexinfo[0].command[4].command "index"
+indexinfo[0].command[5].indexname "title_src"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "product_src"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "product2_src"
+indexinfo[0].command[7].command "index"
+indexinfo[0].command[8].indexname "product3_src"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "attributes"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[10].indexname "attributes"
+indexinfo[0].command[10].command "lowercase"
+indexinfo[0].command[11].indexname "attributes"
+indexinfo[0].command[11].command "normalize"
+indexinfo[0].command[12].indexname "debug"
+indexinfo[0].command[12].command "index"
+indexinfo[0].command[13].indexname "debug"
+indexinfo[0].command[13].command "lowercase"
+indexinfo[0].command[14].indexname "debug"
+indexinfo[0].command[14].command "normalize"
+indexinfo[0].command[15].indexname "location"
+indexinfo[0].command[15].command "default-position"
+indexinfo[0].command[16].indexname "location"
+indexinfo[0].command[16].command "index"
+indexinfo[0].command[17].indexname "location.distance"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "location.distance"
+indexinfo[0].command[18].command "numerical"
+indexinfo[0].command[19].indexname "location.position"
+indexinfo[0].command[19].command "index"
+indexinfo[0].command[20].indexname "location.position"
+indexinfo[0].command[20].command "multivalue"
+indexinfo[0].command[21].indexname "location_zcurve"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "location_zcurve"
+indexinfo[0].command[22].command "attribute"
+indexinfo[0].command[23].indexname "location_zcurve"
+indexinfo[0].command[23].command "fast-search"
+indexinfo[0].command[24].indexname "location_zcurve"
+indexinfo[0].command[24].command "numerical"
+indexinfo[0].command[25].indexname "mysummary"
+indexinfo[0].command[25].command "index"
+indexinfo[0].command[26].indexname "product"
+indexinfo[0].command[26].command "index"
+indexinfo[0].command[27].indexname "product"
+indexinfo[0].command[27].command "lowercase"
+indexinfo[0].command[28].indexname "product"
+indexinfo[0].command[28].command "stem:SHORTEST"
+indexinfo[0].command[29].indexname "product2"
+indexinfo[0].command[29].command "index"
+indexinfo[0].command[30].indexname "product2"
+indexinfo[0].command[30].command "lowercase"
+indexinfo[0].command[31].indexname "product2"
+indexinfo[0].command[31].command "stem:SHORTEST"
+indexinfo[0].command[32].indexname "product3"
+indexinfo[0].command[32].command "index"
+indexinfo[0].command[33].indexname "product3"
+indexinfo[0].command[33].command "lowercase"
+indexinfo[0].command[34].indexname "product3"
+indexinfo[0].command[34].command "stem:SHORTEST"
+indexinfo[0].command[35].indexname "rankfeatures"
+indexinfo[0].command[35].command "index"
+indexinfo[0].command[36].indexname "summaryfeatures"
+indexinfo[0].command[36].command "index"
+indexinfo[0].command[37].indexname "title"
+indexinfo[0].command[37].command "index"
+indexinfo[0].command[38].indexname "titleabstract"
+indexinfo[0].command[38].command "index"
+indexinfo[0].command[39].indexname "default"
+indexinfo[0].command[39].command "index"
+indexinfo[0].command[40].indexname "title"
+indexinfo[0].command[40].command "lowercase"
+indexinfo[0].command[41].indexname "titleabstract"
+indexinfo[0].command[41].command "lowercase"
+indexinfo[0].command[42].indexname "default"
+indexinfo[0].command[42].command "lowercase"
+indexinfo[0].command[43].indexname "title"
+indexinfo[0].command[43].command "normalize"
+indexinfo[0].command[44].indexname "titleabstract"
+indexinfo[0].command[44].command "normalize"
+indexinfo[0].command[45].indexname "default"
+indexinfo[0].command[45].command "normalize"
+indexinfo[0].command[46].indexname "title_s"
+indexinfo[0].command[46].command "index"
+indexinfo[0].alias[0].alias "headline"
+indexinfo[0].alias[0].indexname "title" \ No newline at end of file
diff --git a/config-model/src/test/derived/advanced/rank-profiles.cfg b/config-model/src/test/derived/advanced/rank-profiles.cfg
new file mode 100644
index 00000000000..779ee927952
--- /dev/null
+++ b/config-model/src/test/derived/advanced/rank-profiles.cfg
@@ -0,0 +1,18 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.title"
+rankprofile[0].fef.property[0].value "expdecay(100,12.50)"
+rankprofile[0].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.title"
+rankprofile[0].fef.property[1].value "loggrowth(1500,4000,19)"
+rankprofile[0].fef.property[2].name "nativeProximity.proximityTable.title"
+rankprofile[0].fef.property[2].value "expdecay(5000,3)"
+rankprofile[0].fef.property[3].name "nativeProximity.reverseProximityTable.title"
+rankprofile[0].fef.property[3].value "expdecay(3000,3)"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/advanced/summary.cfg b/config-model/src/test/derived/advanced/summary.cfg
new file mode 100644
index 00000000000..529bebd431b
--- /dev/null
+++ b/config-model/src/test/derived/advanced/summary.cfg
@@ -0,0 +1,33 @@
+defaultsummaryid 1271952241
+classes[0].id 1271952241
+classes[0].name "default"
+classes[0].fields[0].name "debug"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "attributes"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "title_s"
+classes[0].fields[2].type "longstring"
+classes[0].fields[3].name "product"
+classes[0].fields[3].type "longstring"
+classes[0].fields[4].name "product3"
+classes[0].fields[4].type "longstring"
+classes[0].fields[5].name "location.position"
+classes[0].fields[5].type "xmlstring"
+classes[0].fields[6].name "location.distance"
+classes[0].fields[6].type "integer"
+classes[0].fields[7].name "mysummary"
+classes[0].fields[7].type "longstring"
+classes[0].fields[8].name "rankfeatures"
+classes[0].fields[8].type "featuredata"
+classes[0].fields[9].name "summaryfeatures"
+classes[0].fields[9].type "featuredata"
+classes[0].fields[10].name "documentid"
+classes[0].fields[10].type "longstring"
+classes[1].id 472092010
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "location_zcurve"
+classes[1].fields[0].type "int64"
+classes[1].fields[1].name "rankfeatures"
+classes[1].fields[1].type "featuredata"
+classes[1].fields[2].name "summaryfeatures"
+classes[1].fields[2].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/advanced/summarymap.cfg b/config-model/src/test/derived/advanced/summarymap.cfg
new file mode 100644
index 00000000000..ee1a491f8c8
--- /dev/null
+++ b/config-model/src/test/derived/advanced/summarymap.cfg
@@ -0,0 +1,16 @@
+defaultoutputclass -1
+override[0].field "location.position"
+override[0].command "positions"
+override[0].arguments "location_zcurve"
+override[1].field "location.distance"
+override[1].command "absdist"
+override[1].arguments "location_zcurve"
+override[2].field "rankfeatures"
+override[2].command "rankfeatures"
+override[2].arguments ""
+override[3].field "summaryfeatures"
+override[3].command "summaryfeatures"
+override[3].arguments ""
+override[4].field "location_zcurve"
+override[4].command "attribute"
+override[4].arguments "location_zcurve" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsimplicitstruct/annotationsimplicitstruct.sd b/config-model/src/test/derived/annotationsimplicitstruct/annotationsimplicitstruct.sd
new file mode 100755
index 00000000000..37c88f58b6b
--- /dev/null
+++ b/config-model/src/test/derived/annotationsimplicitstruct/annotationsimplicitstruct.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsimplicitstruct {
+
+ document annotationsimplicitstruct {
+
+ annotation banana {
+ field brand type string { }
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/annotationsimplicitstruct/documentmanager.cfg b/config-model/src/test/derived/annotationsimplicitstruct/documentmanager.cfg
new file mode 100755
index 00000000000..db85eb92e9b
--- /dev/null
+++ b/config-model/src/test/derived/annotationsimplicitstruct/documentmanager.cfg
@@ -0,0 +1,49 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 517946310
+datatype[1].structtype[0].name "annotation.banana"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "brand"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id -364910881
+datatype[2].structtype[0].name "annotationsimplicitstruct.header"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "rankfeatures"
+datatype[2].structtype[0].field[0].datatype 2
+datatype[2].structtype[0].field[1].name "summaryfeatures"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[3].id -1503592268
+datatype[3].structtype[0].name "annotationsimplicitstruct.body"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[4].id -2099544992
+datatype[4].documenttype[0].name "annotationsimplicitstruct"
+datatype[4].documenttype[0].version 0
+datatype[4].documenttype[0].inherits[0].name "document"
+datatype[4].documenttype[0].inherits[0].version 0
+datatype[4].documenttype[0].headerstruct -364910881
+datatype[4].documenttype[0].bodystruct -1503592268
+annotationtype[0].id -269517759
+annotationtype[0].name "banana"
+annotationtype[0].datatype 517946310
diff --git a/config-model/src/test/derived/annotationsimplicitstruct/ilscripts.cfg b/config-model/src/test/derived/annotationsimplicitstruct/ilscripts.cfg
new file mode 100644
index 00000000000..b49e52092c2
--- /dev/null
+++ b/config-model/src/test/derived/annotationsimplicitstruct/ilscripts.cfg
@@ -0,0 +1,2 @@
+maxtermoccurrences 100
+ilscript[0].doctype "annotationsimplicitstruct" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsimplicitstruct/index-info.cfg b/config-model/src/test/derived/annotationsimplicitstruct/index-info.cfg
new file mode 100755
index 00000000000..30ee2e6333b
--- /dev/null
+++ b/config-model/src/test/derived/annotationsimplicitstruct/index-info.cfg
@@ -0,0 +1,9 @@
+indexinfo[0].name "annotationsimplicitstruct"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "rankfeatures"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "summaryfeatures"
+indexinfo[0].command[3].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsinheritance/annotationsinheritance.sd b/config-model/src/test/derived/annotationsinheritance/annotationsinheritance.sd
new file mode 100755
index 00000000000..8316e6b7fa5
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance/annotationsinheritance.sd
@@ -0,0 +1,39 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsinheritance {
+
+ document annotationsinheritance {
+
+ annotation fruit {
+ }
+
+ annotation banana inherits fruit {
+ field brand type string { }
+ }
+
+ annotation vehicle {
+ field numwheels type int { }
+ }
+
+ annotation car inherits vehicle {
+ field color type string { }
+ }
+
+ annotation intern inherits employee {
+ field enddate type long { }
+ }
+
+ annotation employee inherits worker {
+ field employeeid type int { }
+ }
+
+ annotation worker inherits person {
+ }
+
+ annotation person {
+ field name type string { }
+ }
+
+ }
+
+
+}
diff --git a/config-model/src/test/derived/annotationsinheritance/documentmanager.cfg b/config-model/src/test/derived/annotationsinheritance/documentmanager.cfg
new file mode 100755
index 00000000000..11e179ec748
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance/documentmanager.cfg
@@ -0,0 +1,135 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 517946310
+datatype[1].structtype[0].name "annotation.banana"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "brand"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id -1339036621
+datatype[2].structtype[0].name "annotation.intern"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "enddate"
+datatype[2].structtype[0].field[0].datatype 4
+datatype[2].structtype[0].inherits[0].name "annotation.employee"
+datatype[2].structtype[0].inherits[0].version 0
+datatype[3].id 249059607
+datatype[3].structtype[0].name "annotation.car"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "color"
+datatype[3].structtype[0].field[0].datatype 2
+datatype[3].structtype[0].inherits[0].name "annotation.vehicle"
+datatype[3].structtype[0].inherits[0].version 0
+datatype[4].id -1466283082
+datatype[4].structtype[0].name "annotation.person"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[4].structtype[0].field[0].name "name"
+datatype[4].structtype[0].field[0].datatype 2
+datatype[5].id -858216177
+datatype[5].structtype[0].name "annotation.employee"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "employeeid"
+datatype[5].structtype[0].field[0].datatype 0
+datatype[5].structtype[0].inherits[0].name "annotation.worker"
+datatype[5].structtype[0].inherits[0].version 0
+datatype[6].id -1874092641
+datatype[6].structtype[0].name "annotation.worker"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[6].structtype[0].inherits[0].name "annotation.person"
+datatype[6].structtype[0].inherits[0].version 0
+datatype[7].id -1047410193
+datatype[7].structtype[0].name "annotation.vehicle"
+datatype[7].structtype[0].version 0
+datatype[7].structtype[0].compresstype NONE
+datatype[7].structtype[0].compresslevel 0
+datatype[7].structtype[0].compressthreshold 95
+datatype[7].structtype[0].compressminsize 800
+datatype[7].structtype[0].field[0].name "numwheels"
+datatype[7].structtype[0].field[0].datatype 0
+datatype[8].id -1406250281
+datatype[8].structtype[0].name "annotationsinheritance.header"
+datatype[8].structtype[0].version 0
+datatype[8].structtype[0].compresstype NONE
+datatype[8].structtype[0].compresslevel 0
+datatype[8].structtype[0].compressthreshold 95
+datatype[8].structtype[0].compressminsize 800
+datatype[8].structtype[0].field[0].name "rankfeatures"
+datatype[8].structtype[0].field[0].datatype 2
+datatype[8].structtype[0].field[1].name "summaryfeatures"
+datatype[8].structtype[0].field[1].datatype 2
+datatype[9].id 1181354668
+datatype[9].structtype[0].name "annotationsinheritance.body"
+datatype[9].structtype[0].version 0
+datatype[9].structtype[0].compresstype NONE
+datatype[9].structtype[0].compresslevel 0
+datatype[9].structtype[0].compressthreshold 95
+datatype[9].structtype[0].compressminsize 800
+datatype[10].id -748546200
+datatype[10].documenttype[0].name "annotationsinheritance"
+datatype[10].documenttype[0].version 0
+datatype[10].documenttype[0].inherits[0].name "document"
+datatype[10].documenttype[0].inherits[0].version 0
+datatype[10].documenttype[0].headerstruct -1406250281
+datatype[10].documenttype[0].bodystruct 1181354668
+annotationtype[0].id -269517759
+annotationtype[0].name "banana"
+annotationtype[0].datatype 517946310
+annotationtype[0].inherits[0].id 877283632
+annotationtype[1].id 855102455
+annotationtype[1].name "intern"
+annotationtype[1].datatype -1339036621
+annotationtype[1].inherits[0].id 804106508
+annotationtype[2].id -973728295
+annotationtype[2].name "car"
+annotationtype[2].datatype 249059607
+annotationtype[2].inherits[0].id 290814930
+annotationtype[3].id 877283632
+annotationtype[3].name "fruit"
+annotationtype[3].datatype -1
+annotationtype[4].id 609952424
+annotationtype[4].name "person"
+annotationtype[4].datatype -1466283082
+annotationtype[5].id 804106508
+annotationtype[5].name "employee"
+annotationtype[5].datatype -858216177
+annotationtype[5].inherits[0].id 881692980
+annotationtype[6].id 881692980
+annotationtype[6].name "worker"
+annotationtype[6].datatype -1874092641
+annotationtype[6].inherits[0].id 609952424
+annotationtype[7].id 290814930
+annotationtype[7].name "vehicle"
+annotationtype[7].datatype -1047410193
diff --git a/config-model/src/test/derived/annotationsinheritance/ilscripts.cfg b/config-model/src/test/derived/annotationsinheritance/ilscripts.cfg
new file mode 100755
index 00000000000..a6b1aabfb47
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance/ilscripts.cfg
@@ -0,0 +1,2 @@
+maxtermoccurrences 100
+ilscript[0].doctype "annotationsinheritance" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsinheritance/index-info.cfg b/config-model/src/test/derived/annotationsinheritance/index-info.cfg
new file mode 100755
index 00000000000..ae1a6462229
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance/index-info.cfg
@@ -0,0 +1,9 @@
+indexinfo[0].name "annotationsinheritance"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "rankfeatures"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "summaryfeatures"
+indexinfo[0].command[3].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsinheritance2/annotationsinheritance2.sd b/config-model/src/test/derived/annotationsinheritance2/annotationsinheritance2.sd
new file mode 100755
index 00000000000..441ea3c4f65
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance2/annotationsinheritance2.sd
@@ -0,0 +1,32 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsinheritance2 {
+
+ document annotationsinheritance2 {
+
+ annotation g {
+ field gfoo type string { }
+ }
+
+ annotation f {
+ }
+
+ annotation e inherits d {
+ }
+
+
+ annotation d inherits c {
+ }
+
+ annotation c {
+ field cfoo type int { }
+ }
+
+ annotation b inherits a {
+ }
+
+ annotation a {
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/annotationsinheritance2/documentmanager.cfg b/config-model/src/test/derived/annotationsinheritance2/documentmanager.cfg
new file mode 100755
index 00000000000..e49d0753296
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance2/documentmanager.cfg
@@ -0,0 +1,97 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 1443831334
+datatype[1].structtype[0].name "annotation.c"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "cfoo"
+datatype[1].structtype[0].field[0].datatype 0
+datatype[2].id 1443832295
+datatype[2].structtype[0].name "annotation.d"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].inherits[0].name "annotation.c"
+datatype[2].structtype[0].inherits[0].version 0
+datatype[3].id 1443833256
+datatype[3].structtype[0].name "annotation.e"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].inherits[0].name "annotation.d"
+datatype[3].structtype[0].inherits[0].version 0
+datatype[4].id 1443835178
+datatype[4].structtype[0].name "annotation.g"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[4].structtype[0].field[0].name "gfoo"
+datatype[4].structtype[0].field[0].datatype 2
+datatype[5].id 424382193
+datatype[5].structtype[0].name "annotationsinheritance2.header"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "rankfeatures"
+datatype[5].structtype[0].field[0].datatype 2
+datatype[5].structtype[0].field[1].name "summaryfeatures"
+datatype[5].structtype[0].field[1].datatype 2
+datatype[6].id 1375438150
+datatype[6].structtype[0].name "annotationsinheritance2.body"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[7].id -1730091890
+datatype[7].documenttype[0].name "annotationsinheritance2"
+datatype[7].documenttype[0].version 0
+datatype[7].documenttype[0].inherits[0].name "document"
+datatype[7].documenttype[0].inherits[0].version 0
+datatype[7].documenttype[0].headerstruct 424382193
+datatype[7].documenttype[0].bodystruct 1375438150
+annotationtype[0].id 1769416289
+annotationtype[0].name "a"
+annotationtype[0].datatype -1
+annotationtype[1].id 1966167951
+annotationtype[1].name "b"
+annotationtype[1].datatype -1
+annotationtype[1].inherits[0].id 1769416289
+annotationtype[2].id 1082875699
+annotationtype[2].name "c"
+annotationtype[2].datatype 1443831334
+annotationtype[3].id 383816109
+annotationtype[3].name "d"
+annotationtype[3].datatype 1443832295
+annotationtype[3].inherits[0].id 1082875699
+annotationtype[4].id -398332878
+annotationtype[4].name "e"
+annotationtype[4].datatype 1443833256
+annotationtype[4].inherits[0].id 383816109
+annotationtype[5].id 422169831
+annotationtype[5].name "f"
+annotationtype[5].datatype -1
+annotationtype[6].id 907314269
+annotationtype[6].name "g"
+annotationtype[6].datatype 1443835178
diff --git a/config-model/src/test/derived/annotationsinheritance2/ilscripts.cfg b/config-model/src/test/derived/annotationsinheritance2/ilscripts.cfg
new file mode 100755
index 00000000000..2717f4ab7e5
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance2/ilscripts.cfg
@@ -0,0 +1,2 @@
+maxtermoccurrences 100
+ilscript[0].doctype "annotationsinheritance2" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsinheritance2/index-info.cfg b/config-model/src/test/derived/annotationsinheritance2/index-info.cfg
new file mode 100755
index 00000000000..5c41ffe1bb7
--- /dev/null
+++ b/config-model/src/test/derived/annotationsinheritance2/index-info.cfg
@@ -0,0 +1,9 @@
+indexinfo[0].name "annotationsinheritance2"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "rankfeatures"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "summaryfeatures"
+indexinfo[0].command[3].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsoutsideofdocument/annotationsoutsideofdocument.sd b/config-model/src/test/derived/annotationsoutsideofdocument/annotationsoutsideofdocument.sd
new file mode 100644
index 00000000000..6b46ec6c86e
--- /dev/null
+++ b/config-model/src/test/derived/annotationsoutsideofdocument/annotationsoutsideofdocument.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsoutsideofdocument {
+
+# (will fail)
+
+annotation foo {}
+
+document {
+
+}
+}
diff --git a/config-model/src/test/derived/annotationspolymorphy/annotationspolymorphy.sd b/config-model/src/test/derived/annotationspolymorphy/annotationspolymorphy.sd
new file mode 100644
index 00000000000..64fd62bc58b
--- /dev/null
+++ b/config-model/src/test/derived/annotationspolymorphy/annotationspolymorphy.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationspolymorphy {
+
+ document annotationspolymorphy {
+
+ annotation super {
+ }
+ annotation sub inherits super {
+ }
+ annotation blah {
+ field a type annotationreference<super> {}
+ }
+
+ }
+}
diff --git a/config-model/src/test/derived/annotationspolymorphy/documentmanager.cfg b/config-model/src/test/derived/annotationspolymorphy/documentmanager.cfg
new file mode 100755
index 00000000000..d612a3b168b
--- /dev/null
+++ b/config-model/src/test/derived/annotationspolymorphy/documentmanager.cfg
@@ -0,0 +1,58 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -2014701853
+datatype[1].annotationreftype[0].annotation "super"
+datatype[2].id -888007918
+datatype[2].structtype[0].name "annotation.blah"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "a"
+datatype[2].structtype[0].field[0].datatype -2014701853
+datatype[3].id -1552577796
+datatype[3].structtype[0].name "annotationspolymorphy.header"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "rankfeatures"
+datatype[3].structtype[0].field[0].datatype 2
+datatype[3].structtype[0].field[1].name "summaryfeatures"
+datatype[3].structtype[0].field[1].datatype 2
+datatype[4].id -570750959
+datatype[4].structtype[0].name "annotationspolymorphy.body"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[5].id -1383624989
+datatype[5].documenttype[0].name "annotationspolymorphy"
+datatype[5].documenttype[0].version 0
+datatype[5].documenttype[0].inherits[0].name "document"
+datatype[5].documenttype[0].inherits[0].version 0
+datatype[5].documenttype[0].headerstruct -1552577796
+datatype[5].documenttype[0].bodystruct -570750959
+annotationtype[0].id 668095690
+annotationtype[0].name "super"
+annotationtype[0].datatype -1
+annotationtype[1].id 119710016
+annotationtype[1].name "sub"
+annotationtype[1].datatype -1
+annotationtype[1].inherits[0].id 668095690
+annotationtype[2].id -1793776935
+annotationtype[2].name "blah"
+annotationtype[2].datatype -888007918
diff --git a/config-model/src/test/derived/annotationspolymorphy/index-info.cfg b/config-model/src/test/derived/annotationspolymorphy/index-info.cfg
new file mode 100755
index 00000000000..67e43f8036f
--- /dev/null
+++ b/config-model/src/test/derived/annotationspolymorphy/index-info.cfg
@@ -0,0 +1,9 @@
+indexinfo[0].name "annotationspolymorphy"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "rankfeatures"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "summaryfeatures"
+indexinfo[0].command[3].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsreference/annotationsreference.sd b/config-model/src/test/derived/annotationsreference/annotationsreference.sd
new file mode 100755
index 00000000000..568a6a1f191
--- /dev/null
+++ b/config-model/src/test/derived/annotationsreference/annotationsreference.sd
@@ -0,0 +1,26 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsreference {
+
+ document annotationsreference {
+
+ annotation banana {
+ field brand type string { }
+ }
+
+ annotation food {
+ field what type annotationreference<banana> { }
+ }
+
+ annotation cyclic {
+ field blah type annotationreference<cyclic> { }
+ }
+
+ annotation a {
+ field foo type annotationreference<b> { }
+ }
+
+ annotation b {
+ }
+
+ }
+}
diff --git a/config-model/src/test/derived/annotationsreference/documentmanager.cfg b/config-model/src/test/derived/annotationsreference/documentmanager.cfg
new file mode 100755
index 00000000000..3218262b54d
--- /dev/null
+++ b/config-model/src/test/derived/annotationsreference/documentmanager.cfg
@@ -0,0 +1,94 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 517946310
+datatype[1].structtype[0].name "annotation.banana"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "brand"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id 400622300
+datatype[2].annotationreftype[0].annotation "b"
+datatype[3].id 1443829412
+datatype[3].structtype[0].name "annotation.a"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "foo"
+datatype[3].structtype[0].field[0].datatype 400622300
+datatype[4].id -808460615
+datatype[4].annotationreftype[0].annotation "banana"
+datatype[5].id -770307521
+datatype[5].structtype[0].name "annotation.food"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "what"
+datatype[5].structtype[0].field[0].datatype -808460615
+datatype[6].id 756306917
+datatype[6].annotationreftype[0].annotation "cyclic"
+datatype[7].id 1781099546
+datatype[7].structtype[0].name "annotation.cyclic"
+datatype[7].structtype[0].version 0
+datatype[7].structtype[0].compresstype NONE
+datatype[7].structtype[0].compresslevel 0
+datatype[7].structtype[0].compressthreshold 95
+datatype[7].structtype[0].compressminsize 800
+datatype[7].structtype[0].field[0].name "blah"
+datatype[7].structtype[0].field[0].datatype 756306917
+datatype[8].id 571255414
+datatype[8].structtype[0].name "annotationsreference.header"
+datatype[8].structtype[0].version 0
+datatype[8].structtype[0].compresstype NONE
+datatype[8].structtype[0].compresslevel 0
+datatype[8].structtype[0].compressthreshold 95
+datatype[8].structtype[0].compressminsize 800
+datatype[8].structtype[0].field[0].name "rankfeatures"
+datatype[8].structtype[0].field[0].datatype 2
+datatype[8].structtype[0].field[1].name "summaryfeatures"
+datatype[8].structtype[0].field[1].datatype 2
+datatype[9].id 1692909067
+datatype[9].structtype[0].name "annotationsreference.body"
+datatype[9].structtype[0].version 0
+datatype[9].structtype[0].compresstype NONE
+datatype[9].structtype[0].compresslevel 0
+datatype[9].structtype[0].compressthreshold 95
+datatype[9].structtype[0].compressminsize 800
+datatype[10].id -1448377175
+datatype[10].documenttype[0].name "annotationsreference"
+datatype[10].documenttype[0].version 0
+datatype[10].documenttype[0].inherits[0].name "document"
+datatype[10].documenttype[0].inherits[0].version 0
+datatype[10].documenttype[0].headerstruct 571255414
+datatype[10].documenttype[0].bodystruct 1692909067
+annotationtype[0].id -269517759
+annotationtype[0].name "banana"
+annotationtype[0].datatype 517946310
+annotationtype[1].id 1769416289
+annotationtype[1].name "a"
+annotationtype[1].datatype 1443829412
+annotationtype[2].id 1966167951
+annotationtype[2].name "b"
+annotationtype[2].datatype -1
+annotationtype[3].id 1918102169
+annotationtype[3].name "food"
+annotationtype[3].datatype -770307521
+annotationtype[4].id -1569831750
+annotationtype[4].name "cyclic"
+annotationtype[4].datatype 1781099546
diff --git a/config-model/src/test/derived/annotationsreference/ilscripts.cfg b/config-model/src/test/derived/annotationsreference/ilscripts.cfg
new file mode 100755
index 00000000000..2297994c727
--- /dev/null
+++ b/config-model/src/test/derived/annotationsreference/ilscripts.cfg
@@ -0,0 +1,2 @@
+maxtermoccurrences 100
+ilscript[0].doctype "annotationsreference" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsreference/index-info.cfg b/config-model/src/test/derived/annotationsreference/index-info.cfg
new file mode 100755
index 00000000000..0dfc76861e1
--- /dev/null
+++ b/config-model/src/test/derived/annotationsreference/index-info.cfg
@@ -0,0 +1,9 @@
+indexinfo[0].name "annotationsreference"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "rankfeatures"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "summaryfeatures"
+indexinfo[0].command[3].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsreference2/annotationsreference2.sd b/config-model/src/test/derived/annotationsreference2/annotationsreference2.sd
new file mode 100644
index 00000000000..5b4a2eb0562
--- /dev/null
+++ b/config-model/src/test/derived/annotationsreference2/annotationsreference2.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationreference2 {
+ document annotationreference2 {
+ annotation foo { }
+ annotation bar {
+ field baz type array<annotationreference<foo>> { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/annotationssimple/annotationssimple.sd b/config-model/src/test/derived/annotationssimple/annotationssimple.sd
new file mode 100755
index 00000000000..82bcd55b673
--- /dev/null
+++ b/config-model/src/test/derived/annotationssimple/annotationssimple.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationssimple {
+
+ document annotationssimple {
+ annotation banana { }
+ }
+
+}
diff --git a/config-model/src/test/derived/annotationssimple/documentmanager.cfg b/config-model/src/test/derived/annotationssimple/documentmanager.cfg
new file mode 100755
index 00000000000..54832bb97f9
--- /dev/null
+++ b/config-model/src/test/derived/annotationssimple/documentmanager.cfg
@@ -0,0 +1,40 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -1205708249
+datatype[1].structtype[0].name "annotationssimple.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "rankfeatures"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[1].structtype[0].field[1].name "summaryfeatures"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[2].id -682121732
+datatype[2].structtype[0].name "annotationssimple.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[3].id -1584092648
+datatype[3].documenttype[0].name "annotationssimple"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct -1205708249
+datatype[3].documenttype[0].bodystruct -682121732
+annotationtype[0].id -269517759
+annotationtype[0].name "banana"
+annotationtype[0].datatype -1
diff --git a/config-model/src/test/derived/annotationssimple/ilscripts.cfg b/config-model/src/test/derived/annotationssimple/ilscripts.cfg
new file mode 100644
index 00000000000..a234feff99d
--- /dev/null
+++ b/config-model/src/test/derived/annotationssimple/ilscripts.cfg
@@ -0,0 +1,2 @@
+maxtermoccurrences 100
+ilscript[0].doctype "annotationssimple" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationssimple/index-info.cfg b/config-model/src/test/derived/annotationssimple/index-info.cfg
new file mode 100755
index 00000000000..84c18bc082f
--- /dev/null
+++ b/config-model/src/test/derived/annotationssimple/index-info.cfg
@@ -0,0 +1,9 @@
+indexinfo[0].name "annotationssimple"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "rankfeatures"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "summaryfeatures"
+indexinfo[0].command[3].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/annotationsstruct/annotationsstruct.sd b/config-model/src/test/derived/annotationsstruct/annotationsstruct.sd
new file mode 100644
index 00000000000..b4ba809d93b
--- /dev/null
+++ b/config-model/src/test/derived/annotationsstruct/annotationsstruct.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsstruct {
+ document annotationsstruct {
+ struct my_struct {
+ field my_structfield type string { }
+ }
+ annotation my_anno {
+ field my_annofield type my_struct { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/annotationsstruct/documentmanager.cfg b/config-model/src/test/derived/annotationsstruct/documentmanager.cfg
new file mode 100644
index 00000000000..f3c93b31285
--- /dev/null
+++ b/config-model/src/test/derived/annotationsstruct/documentmanager.cfg
@@ -0,0 +1,58 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 1293792650
+datatype[1].structtype[0].name "my_struct"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "my_structfield"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id -1080124700
+datatype[2].structtype[0].name "annotation.my_anno"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "my_annofield"
+datatype[2].structtype[0].field[0].datatype 1293792650
+datatype[3].id 1341437796
+datatype[3].structtype[0].name "annotationsstruct.header"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "rankfeatures"
+datatype[3].structtype[0].field[0].datatype 2
+datatype[3].structtype[0].field[1].name "summaryfeatures"
+datatype[3].structtype[0].field[1].datatype 2
+datatype[4].id -1180029319
+datatype[4].structtype[0].name "annotationsstruct.body"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[5].id -263977093
+datatype[5].documenttype[0].name "annotationsstruct"
+datatype[5].documenttype[0].version 0
+datatype[5].documenttype[0].inherits[0].name "document"
+datatype[5].documenttype[0].inherits[0].version 0
+datatype[5].documenttype[0].headerstruct 1341437796
+datatype[5].documenttype[0].bodystruct -1180029319
+annotationtype[0].id -160036815
+annotationtype[0].name "my_anno"
+annotationtype[0].datatype -1080124700
diff --git a/config-model/src/test/derived/annotationsstructarray/annotationsstructarray.sd b/config-model/src/test/derived/annotationsstructarray/annotationsstructarray.sd
new file mode 100644
index 00000000000..e54aae45caa
--- /dev/null
+++ b/config-model/src/test/derived/annotationsstructarray/annotationsstructarray.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsstructarray {
+ document annotationsstructarray {
+ struct my_struct {
+ field my_structfield type string { }
+ }
+ annotation my_anno {
+ field my_annofield type array<my_struct> { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/annotationsstructarray/documentmanager.cfg b/config-model/src/test/derived/annotationsstructarray/documentmanager.cfg
new file mode 100644
index 00000000000..01e3945fc2a
--- /dev/null
+++ b/config-model/src/test/derived/annotationsstructarray/documentmanager.cfg
@@ -0,0 +1,60 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 1293792650
+datatype[1].structtype[0].name "my_struct"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "my_structfield"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id 754837689
+datatype[2].arraytype[0].datatype 1293792650
+datatype[3].id -1080124700
+datatype[3].structtype[0].name "annotation.my_anno"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "my_annofield"
+datatype[3].structtype[0].field[0].datatype 754837689
+datatype[4].id 94945597
+datatype[4].structtype[0].name "annotationsstructarray.header"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[4].structtype[0].field[0].name "rankfeatures"
+datatype[4].structtype[0].field[0].datatype 2
+datatype[4].structtype[0].field[1].name "summaryfeatures"
+datatype[4].structtype[0].field[1].datatype 2
+datatype[5].id 1616435858
+datatype[5].structtype[0].name "annotationsstructarray.body"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[6].id 1593733058
+datatype[6].documenttype[0].name "annotationsstructarray"
+datatype[6].documenttype[0].version 0
+datatype[6].documenttype[0].inherits[0].name "document"
+datatype[6].documenttype[0].inherits[0].version 0
+datatype[6].documenttype[0].headerstruct 94945597
+datatype[6].documenttype[0].bodystruct 1616435858
+annotationtype[0].id -160036815
+annotationtype[0].name "my_anno"
+annotationtype[0].datatype -1080124700
diff --git a/config-model/src/test/derived/arrays/arrays.sd b/config-model/src/test/derived/arrays/arrays.sd
new file mode 100644
index 00000000000..c5ce810bc0c
--- /dev/null
+++ b/config-model/src/test/derived/arrays/arrays.sd
@@ -0,0 +1,32 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search arrays {
+
+ document arrays {
+
+ field tags type array<string> {
+ indexing: summary | index
+ }
+
+ field ratings type int[] {
+ indexing: summary | index
+ }
+
+ field a type string {
+ indexing: index
+ }
+
+ field b type array<string> {
+ indexing: index | attribute
+ }
+
+ field c type weightedset<string> {
+ indexing: summary | index
+ }
+
+ }
+
+ fieldset default {
+ fields: a, b, c
+ }
+
+}
diff --git a/config-model/src/test/derived/arrays/documentmanager.cfg b/config-model/src/test/derived/arrays/documentmanager.cfg
new file mode 100644
index 00000000000..68506844b5d
--- /dev/null
+++ b/config-model/src/test/derived/arrays/documentmanager.cfg
@@ -0,0 +1,63 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -1486737430
+datatype[1].arraytype[0].datatype 2
+datatype[2].id -1245117006
+datatype[2].arraytype[0].datatype 0
+datatype[3].id 1328286588
+datatype[3].weightedsettype[0].datatype 2
+datatype[3].weightedsettype[0].createifnonexistant false
+datatype[3].weightedsettype[0].removeifzero false
+datatype[4].id 1081627459
+datatype[4].structtype[0].name "arrays.header"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[4].structtype[0].field[0].name "tags"
+datatype[4].structtype[0].field[0].datatype -1486737430
+datatype[4].structtype[0].field[1].name "ratings"
+datatype[4].structtype[0].field[1].datatype -1245117006
+datatype[4].structtype[0].field[2].name "a"
+datatype[4].structtype[0].field[2].datatype 2
+datatype[4].structtype[0].field[3].name "b"
+datatype[4].structtype[0].field[3].datatype -1486737430
+datatype[4].structtype[0].field[4].name "c"
+datatype[4].structtype[0].field[4].datatype 1328286588
+datatype[4].structtype[0].field[5].name "rankfeatures"
+datatype[4].structtype[0].field[5].datatype 2
+datatype[4].structtype[0].field[6].name "summaryfeatures"
+datatype[4].structtype[0].field[6].datatype 2
+datatype[5].id -1747896808
+datatype[5].structtype[0].name "arrays.body"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[6].id -1292863364
+datatype[6].documenttype[0].name "arrays"
+datatype[6].documenttype[0].version 0
+datatype[6].documenttype[0].inherits[0].name "document"
+datatype[6].documenttype[0].inherits[0].version 0
+datatype[6].documenttype[0].headerstruct 1081627459
+datatype[6].documenttype[0].bodystruct -1747896808
+datatype[6].documenttype[0].fieldsets{default}.fields[0] "a"
+datatype[6].documenttype[0].fieldsets{default}.fields[1] "b"
+datatype[6].documenttype[0].fieldsets{default}.fields[2] "c"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[0] "a"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[1] "b"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[2] "c"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[3] "ratings"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[4] "tags"
diff --git a/config-model/src/test/derived/arrays/ilscripts.cfg b/config-model/src/test/derived/arrays/ilscripts.cfg
new file mode 100644
index 00000000000..d8fbe752676
--- /dev/null
+++ b/config-model/src/test/derived/arrays/ilscripts.cfg
@@ -0,0 +1,12 @@
+maxtermoccurrences 100
+ilscript[0].doctype "arrays"
+ilscript[0].docfield[0] "tags"
+ilscript[0].docfield[1] "ratings"
+ilscript[0].docfield[2] "a"
+ilscript[0].docfield[3] "b"
+ilscript[0].docfield[4] "c"
+ilscript[0].content[0] "clear_state | guard { input tags | for_each { tokenize normalize stem:\"SHORTEST\" } | summary tags | index tags; }"
+ilscript[0].content[1] "clear_state | guard { input ratings | summary ratings | attribute ratings; }"
+ilscript[0].content[2] "clear_state | guard { input a | tokenize normalize stem:\"SHORTEST\" | index a; }"
+ilscript[0].content[3] "clear_state | guard { input b | for_each { tokenize normalize stem:\"SHORTEST\" } | index b | attribute b; }"
+ilscript[0].content[4] "clear_state | guard { input c | for_each { tokenize normalize stem:\"SHORTEST\" } | summary c | index c; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/arrays/index-info.cfg b/config-model/src/test/derived/arrays/index-info.cfg
new file mode 100644
index 00000000000..f5f65700d89
--- /dev/null
+++ b/config-model/src/test/derived/arrays/index-info.cfg
@@ -0,0 +1,63 @@
+indexinfo[].name "arrays"
+indexinfo[].command[].indexname "sddocname"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "sddocname"
+indexinfo[].command[].command "word"
+indexinfo[].command[].indexname "tags"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "tags"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "tags"
+indexinfo[].command[].command "multivalue"
+indexinfo[].command[].indexname "tags"
+indexinfo[].command[].command "stem:SHORTEST"
+indexinfo[].command[].indexname "tags"
+indexinfo[].command[].command "normalize"
+indexinfo[].command[].indexname "ratings"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "ratings"
+indexinfo[].command[].command "multivalue"
+indexinfo[].command[].indexname "ratings"
+indexinfo[].command[].command "attribute"
+indexinfo[].command[].indexname "default"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "default"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "default"
+indexinfo[].command[].command "stem:SHORTEST"
+indexinfo[].command[].indexname "default"
+indexinfo[].command[].command "normalize"
+indexinfo[].command[].indexname "default"
+indexinfo[].command[].command "multivalue"
+indexinfo[].command[].indexname "a"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "a"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "a"
+indexinfo[].command[].command "stem:SHORTEST"
+indexinfo[].command[].indexname "a"
+indexinfo[].command[].command "normalize"
+indexinfo[].command[].indexname "b"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "b"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "b"
+indexinfo[].command[].command "multivalue"
+indexinfo[].command[].indexname "b"
+indexinfo[].command[].command "stem:SHORTEST"
+indexinfo[].command[].indexname "b"
+indexinfo[].command[].command "normalize"
+indexinfo[].command[].indexname "c"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "c"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "c"
+indexinfo[].command[].command "multivalue"
+indexinfo[].command[].indexname "c"
+indexinfo[].command[].command "stem:SHORTEST"
+indexinfo[].command[].indexname "c"
+indexinfo[].command[].command "normalize"
+indexinfo[].command[].indexname "rankfeatures"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "summaryfeatures"
+indexinfo[].command[].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributeprefetch/attributeprefetch.sd b/config-model/src/test/derived/attributeprefetch/attributeprefetch.sd
new file mode 100644
index 00000000000..25804ba9728
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/attributeprefetch.sd
@@ -0,0 +1,86 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search prefetch {
+ document prefetch {
+ field singlebyte type byte {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field multibyte type array<byte> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field wsbyte type weightedset<byte> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field singleint type int {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field multiint type array<int> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field wsint type weightedset<int> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field singlelong type long {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field multilong type array<long> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field wslong type weightedset<long> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field singlefloat type float {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field multifloat type array<float> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field wsfloat type weightedset<float> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field singledouble type double {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field multidouble type array<double> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field wsdouble type weightedset<double> {
+ indexing: attribute
+ attribute: prefetch
+ }
+ field singlestring type string {
+ indexing: attribute
+ attribute: prefetch
+ match {
+ token
+ }
+ }
+ field multistring type array<string> {
+ indexing: attribute
+ attribute: prefetch
+ match {
+ token
+ }
+ }
+ field wsstring type weightedset<string> {
+ indexing: attribute
+ attribute: prefetch
+ match {
+ token
+ }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/attributeprefetch/attributes.cfg b/config-model/src/test/derived/attributeprefetch/attributes.cfg
new file mode 100644
index 00000000000..16c2d9e404e
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/attributes.cfg
@@ -0,0 +1,342 @@
+attribute[0].name "singlebyte"
+attribute[0].datatype INT8
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "multibyte"
+attribute[1].datatype INT8
+attribute[1].collectiontype ARRAY
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "wsbyte"
+attribute[2].datatype INT8
+attribute[2].collectiontype WEIGHTEDSET
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending true
+attribute[2].sortfunction UCA
+attribute[2].sortstrength PRIMARY
+attribute[2].sortlocale ""
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype ""
+attribute[3].name "singleint"
+attribute[3].datatype INT32
+attribute[3].collectiontype SINGLE
+attribute[3].removeifzero false
+attribute[3].createifnonexistent false
+attribute[3].fastsearch false
+attribute[3].huge false
+attribute[3].sortascending true
+attribute[3].sortfunction UCA
+attribute[3].sortstrength PRIMARY
+attribute[3].sortlocale ""
+attribute[3].enablebitvectors false
+attribute[3].enableonlybitvector false
+attribute[3].fastaccess false
+attribute[3].arity 8
+attribute[3].lowerbound -9223372036854775808
+attribute[3].upperbound 9223372036854775807
+attribute[3].densepostinglistthreshold 0.4
+attribute[3].tensortype ""
+attribute[4].name "multiint"
+attribute[4].datatype INT32
+attribute[4].collectiontype ARRAY
+attribute[4].removeifzero false
+attribute[4].createifnonexistent false
+attribute[4].fastsearch false
+attribute[4].huge false
+attribute[4].sortascending true
+attribute[4].sortfunction UCA
+attribute[4].sortstrength PRIMARY
+attribute[4].sortlocale ""
+attribute[4].enablebitvectors false
+attribute[4].enableonlybitvector false
+attribute[4].fastaccess false
+attribute[4].arity 8
+attribute[4].lowerbound -9223372036854775808
+attribute[4].upperbound 9223372036854775807
+attribute[4].densepostinglistthreshold 0.4
+attribute[4].tensortype ""
+attribute[5].name "wsint"
+attribute[5].datatype INT32
+attribute[5].collectiontype WEIGHTEDSET
+attribute[5].removeifzero false
+attribute[5].createifnonexistent false
+attribute[5].fastsearch false
+attribute[5].huge false
+attribute[5].sortascending true
+attribute[5].sortfunction UCA
+attribute[5].sortstrength PRIMARY
+attribute[5].sortlocale ""
+attribute[5].enablebitvectors false
+attribute[5].enableonlybitvector false
+attribute[5].fastaccess false
+attribute[5].arity 8
+attribute[5].lowerbound -9223372036854775808
+attribute[5].upperbound 9223372036854775807
+attribute[5].densepostinglistthreshold 0.4
+attribute[5].tensortype ""
+attribute[6].name "singlelong"
+attribute[6].datatype INT64
+attribute[6].collectiontype SINGLE
+attribute[6].removeifzero false
+attribute[6].createifnonexistent false
+attribute[6].fastsearch false
+attribute[6].huge false
+attribute[6].sortascending true
+attribute[6].sortfunction UCA
+attribute[6].sortstrength PRIMARY
+attribute[6].sortlocale ""
+attribute[6].enablebitvectors false
+attribute[6].enableonlybitvector false
+attribute[6].fastaccess false
+attribute[6].arity 8
+attribute[6].lowerbound -9223372036854775808
+attribute[6].upperbound 9223372036854775807
+attribute[6].densepostinglistthreshold 0.4
+attribute[6].tensortype ""
+attribute[7].name "multilong"
+attribute[7].datatype INT64
+attribute[7].collectiontype ARRAY
+attribute[7].removeifzero false
+attribute[7].createifnonexistent false
+attribute[7].fastsearch false
+attribute[7].huge false
+attribute[7].sortascending true
+attribute[7].sortfunction UCA
+attribute[7].sortstrength PRIMARY
+attribute[7].sortlocale ""
+attribute[7].enablebitvectors false
+attribute[7].enableonlybitvector false
+attribute[7].fastaccess false
+attribute[7].arity 8
+attribute[7].lowerbound -9223372036854775808
+attribute[7].upperbound 9223372036854775807
+attribute[7].densepostinglistthreshold 0.4
+attribute[7].tensortype ""
+attribute[8].name "wslong"
+attribute[8].datatype INT64
+attribute[8].collectiontype WEIGHTEDSET
+attribute[8].removeifzero false
+attribute[8].createifnonexistent false
+attribute[8].fastsearch false
+attribute[8].huge false
+attribute[8].sortascending true
+attribute[8].sortfunction UCA
+attribute[8].sortstrength PRIMARY
+attribute[8].sortlocale ""
+attribute[8].enablebitvectors false
+attribute[8].enableonlybitvector false
+attribute[8].fastaccess false
+attribute[8].arity 8
+attribute[8].lowerbound -9223372036854775808
+attribute[8].upperbound 9223372036854775807
+attribute[8].densepostinglistthreshold 0.4
+attribute[8].tensortype ""
+attribute[9].name "singlefloat"
+attribute[9].datatype FLOAT
+attribute[9].collectiontype SINGLE
+attribute[9].removeifzero false
+attribute[9].createifnonexistent false
+attribute[9].fastsearch false
+attribute[9].huge false
+attribute[9].sortascending true
+attribute[9].sortfunction UCA
+attribute[9].sortstrength PRIMARY
+attribute[9].sortlocale ""
+attribute[9].enablebitvectors false
+attribute[9].enableonlybitvector false
+attribute[9].fastaccess false
+attribute[9].arity 8
+attribute[9].lowerbound -9223372036854775808
+attribute[9].upperbound 9223372036854775807
+attribute[9].densepostinglistthreshold 0.4
+attribute[9].tensortype ""
+attribute[10].name "multifloat"
+attribute[10].datatype FLOAT
+attribute[10].collectiontype ARRAY
+attribute[10].removeifzero false
+attribute[10].createifnonexistent false
+attribute[10].fastsearch false
+attribute[10].huge false
+attribute[10].sortascending true
+attribute[10].sortfunction UCA
+attribute[10].sortstrength PRIMARY
+attribute[10].sortlocale ""
+attribute[10].enablebitvectors false
+attribute[10].enableonlybitvector false
+attribute[10].fastaccess false
+attribute[10].arity 8
+attribute[10].lowerbound -9223372036854775808
+attribute[10].upperbound 9223372036854775807
+attribute[10].densepostinglistthreshold 0.4
+attribute[10].tensortype ""
+attribute[11].name "wsfloat"
+attribute[11].datatype FLOAT
+attribute[11].collectiontype WEIGHTEDSET
+attribute[11].removeifzero false
+attribute[11].createifnonexistent false
+attribute[11].fastsearch false
+attribute[11].huge false
+attribute[11].sortascending true
+attribute[11].sortfunction UCA
+attribute[11].sortstrength PRIMARY
+attribute[11].sortlocale ""
+attribute[11].enablebitvectors false
+attribute[11].enableonlybitvector false
+attribute[11].fastaccess false
+attribute[11].arity 8
+attribute[11].lowerbound -9223372036854775808
+attribute[11].upperbound 9223372036854775807
+attribute[11].densepostinglistthreshold 0.4
+attribute[11].tensortype ""
+attribute[12].name "singledouble"
+attribute[12].datatype DOUBLE
+attribute[12].collectiontype SINGLE
+attribute[12].removeifzero false
+attribute[12].createifnonexistent false
+attribute[12].fastsearch false
+attribute[12].huge false
+attribute[12].sortascending true
+attribute[12].sortfunction UCA
+attribute[12].sortstrength PRIMARY
+attribute[12].sortlocale ""
+attribute[12].enablebitvectors false
+attribute[12].enableonlybitvector false
+attribute[12].fastaccess false
+attribute[12].arity 8
+attribute[12].lowerbound -9223372036854775808
+attribute[12].upperbound 9223372036854775807
+attribute[12].densepostinglistthreshold 0.4
+attribute[12].tensortype ""
+attribute[13].name "multidouble"
+attribute[13].datatype DOUBLE
+attribute[13].collectiontype ARRAY
+attribute[13].removeifzero false
+attribute[13].createifnonexistent false
+attribute[13].fastsearch false
+attribute[13].huge false
+attribute[13].sortascending true
+attribute[13].sortfunction UCA
+attribute[13].sortstrength PRIMARY
+attribute[13].sortlocale ""
+attribute[13].enablebitvectors false
+attribute[13].enableonlybitvector false
+attribute[13].fastaccess false
+attribute[13].arity 8
+attribute[13].lowerbound -9223372036854775808
+attribute[13].upperbound 9223372036854775807
+attribute[13].densepostinglistthreshold 0.4
+attribute[13].tensortype ""
+attribute[14].name "wsdouble"
+attribute[14].datatype DOUBLE
+attribute[14].collectiontype WEIGHTEDSET
+attribute[14].removeifzero false
+attribute[14].createifnonexistent false
+attribute[14].fastsearch false
+attribute[14].huge false
+attribute[14].sortascending true
+attribute[14].sortfunction UCA
+attribute[14].sortstrength PRIMARY
+attribute[14].sortlocale ""
+attribute[14].enablebitvectors false
+attribute[14].enableonlybitvector false
+attribute[14].fastaccess false
+attribute[14].arity 8
+attribute[14].lowerbound -9223372036854775808
+attribute[14].upperbound 9223372036854775807
+attribute[14].densepostinglistthreshold 0.4
+attribute[14].tensortype ""
+attribute[15].name "singlestring"
+attribute[15].datatype STRING
+attribute[15].collectiontype SINGLE
+attribute[15].removeifzero false
+attribute[15].createifnonexistent false
+attribute[15].fastsearch false
+attribute[15].huge false
+attribute[15].sortascending true
+attribute[15].sortfunction UCA
+attribute[15].sortstrength PRIMARY
+attribute[15].sortlocale ""
+attribute[15].enablebitvectors false
+attribute[15].enableonlybitvector false
+attribute[15].fastaccess false
+attribute[15].arity 8
+attribute[15].lowerbound -9223372036854775808
+attribute[15].upperbound 9223372036854775807
+attribute[15].densepostinglistthreshold 0.4
+attribute[15].tensortype ""
+attribute[16].name "multistring"
+attribute[16].datatype STRING
+attribute[16].collectiontype ARRAY
+attribute[16].removeifzero false
+attribute[16].createifnonexistent false
+attribute[16].fastsearch false
+attribute[16].huge false
+attribute[16].sortascending true
+attribute[16].sortfunction UCA
+attribute[16].sortstrength PRIMARY
+attribute[16].sortlocale ""
+attribute[16].enablebitvectors false
+attribute[16].enableonlybitvector false
+attribute[16].fastaccess false
+attribute[16].arity 8
+attribute[16].lowerbound -9223372036854775808
+attribute[16].upperbound 9223372036854775807
+attribute[16].densepostinglistthreshold 0.4
+attribute[16].tensortype ""
+attribute[17].name "wsstring"
+attribute[17].datatype STRING
+attribute[17].collectiontype WEIGHTEDSET
+attribute[17].removeifzero false
+attribute[17].createifnonexistent false
+attribute[17].fastsearch false
+attribute[17].huge false
+attribute[17].sortascending true
+attribute[17].sortfunction UCA
+attribute[17].sortstrength PRIMARY
+attribute[17].sortlocale ""
+attribute[17].enablebitvectors false
+attribute[17].enableonlybitvector false
+attribute[17].fastaccess false
+attribute[17].arity 8
+attribute[17].lowerbound -9223372036854775808
+attribute[17].upperbound 9223372036854775807
+attribute[17].densepostinglistthreshold 0.4
+attribute[17].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributeprefetch/documentmanager.cfg b/config-model/src/test/derived/attributeprefetch/documentmanager.cfg
new file mode 100644
index 00000000000..92558a62509
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/documentmanager.cfg
@@ -0,0 +1,127 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 49942803
+datatype[1].arraytype[0].datatype 16
+datatype[2].id -1068914395
+datatype[2].weightedsettype[0].datatype 16
+datatype[2].weightedsettype[0].createifnonexistant false
+datatype[2].weightedsettype[0].removeifzero false
+datatype[3].id -1245117006
+datatype[3].arraytype[0].datatype 0
+datatype[4].id 519906144
+datatype[4].weightedsettype[0].datatype 0
+datatype[4].weightedsettype[0].createifnonexistant false
+datatype[4].weightedsettype[0].removeifzero false
+datatype[5].id 58874399
+datatype[5].arraytype[0].datatype 4
+datatype[6].id -1059982799
+datatype[6].weightedsettype[0].datatype 4
+datatype[6].weightedsettype[0].createifnonexistant false
+datatype[6].weightedsettype[0].removeifzero false
+datatype[7].id 1650586661
+datatype[7].arraytype[0].datatype 1
+datatype[8].id 1325751891
+datatype[8].weightedsettype[0].datatype 1
+datatype[8].weightedsettype[0].createifnonexistant false
+datatype[8].weightedsettype[0].removeifzero false
+datatype[9].id -2054976470
+datatype[9].arraytype[0].datatype 5
+datatype[10].id 760047548
+datatype[10].weightedsettype[0].datatype 5
+datatype[10].weightedsettype[0].createifnonexistant false
+datatype[10].weightedsettype[0].removeifzero false
+datatype[11].id -1486737430
+datatype[11].arraytype[0].datatype 2
+datatype[12].id 1328286588
+datatype[12].weightedsettype[0].datatype 2
+datatype[12].weightedsettype[0].createifnonexistant false
+datatype[12].weightedsettype[0].removeifzero false
+datatype[13].id -109105370
+datatype[13].structtype[0].name "prefetch.header"
+datatype[13].structtype[0].version 0
+datatype[13].structtype[0].compresstype NONE
+datatype[13].structtype[0].compresslevel 0
+datatype[13].structtype[0].compressthreshold 95
+datatype[13].structtype[0].compressminsize 800
+datatype[13].structtype[0].field[0].name "singlebyte"
+datatype[13].structtype[0].field[0].datatype 16
+datatype[13].structtype[0].field[1].name "multibyte"
+datatype[13].structtype[0].field[1].datatype 49942803
+datatype[13].structtype[0].field[2].name "wsbyte"
+datatype[13].structtype[0].field[2].datatype -1068914395
+datatype[13].structtype[0].field[3].name "singleint"
+datatype[13].structtype[0].field[3].datatype 0
+datatype[13].structtype[0].field[4].name "multiint"
+datatype[13].structtype[0].field[4].datatype -1245117006
+datatype[13].structtype[0].field[5].name "wsint"
+datatype[13].structtype[0].field[5].datatype 519906144
+datatype[13].structtype[0].field[6].name "singlelong"
+datatype[13].structtype[0].field[6].datatype 4
+datatype[13].structtype[0].field[7].name "multilong"
+datatype[13].structtype[0].field[7].datatype 58874399
+datatype[13].structtype[0].field[8].name "wslong"
+datatype[13].structtype[0].field[8].datatype -1059982799
+datatype[13].structtype[0].field[9].name "singlefloat"
+datatype[13].structtype[0].field[9].datatype 1
+datatype[13].structtype[0].field[10].name "multifloat"
+datatype[13].structtype[0].field[10].datatype 1650586661
+datatype[13].structtype[0].field[11].name "wsfloat"
+datatype[13].structtype[0].field[11].datatype 1325751891
+datatype[13].structtype[0].field[12].name "singledouble"
+datatype[13].structtype[0].field[12].datatype 5
+datatype[13].structtype[0].field[13].name "multidouble"
+datatype[13].structtype[0].field[13].datatype -2054976470
+datatype[13].structtype[0].field[14].name "wsdouble"
+datatype[13].structtype[0].field[14].datatype 760047548
+datatype[13].structtype[0].field[15].name "singlestring"
+datatype[13].structtype[0].field[15].datatype 2
+datatype[13].structtype[0].field[16].name "multistring"
+datatype[13].structtype[0].field[16].datatype -1486737430
+datatype[13].structtype[0].field[17].name "wsstring"
+datatype[13].structtype[0].field[17].datatype 1328286588
+datatype[13].structtype[0].field[18].name "rankfeatures"
+datatype[13].structtype[0].field[18].datatype 2
+datatype[13].structtype[0].field[19].name "summaryfeatures"
+datatype[13].structtype[0].field[19].datatype 2
+datatype[14].id 932425403
+datatype[14].structtype[0].name "prefetch.body"
+datatype[14].structtype[0].version 0
+datatype[14].structtype[0].compresstype NONE
+datatype[14].structtype[0].compresslevel 0
+datatype[14].structtype[0].compressthreshold 95
+datatype[14].structtype[0].compressminsize 800
+datatype[15].id -1458051591
+datatype[15].documenttype[0].name "prefetch"
+datatype[15].documenttype[0].version 0
+datatype[15].documenttype[0].inherits[0].name "document"
+datatype[15].documenttype[0].inherits[0].version 0
+datatype[15].documenttype[0].headerstruct -109105370
+datatype[15].documenttype[0].bodystruct 932425403
+datatype[15].documenttype[0].fieldsets{[document]}.fields[0] "multibyte"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[1] "multidouble"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[2] "multifloat"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[3] "multiint"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[4] "multilong"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[5] "multistring"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[6] "singlebyte"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[7] "singledouble"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[8] "singlefloat"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[9] "singleint"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[10] "singlelong"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[11] "singlestring"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[12] "wsbyte"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[13] "wsdouble"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[14] "wsfloat"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[15] "wsint"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[16] "wslong"
+datatype[15].documenttype[0].fieldsets{[document]}.fields[17] "wsstring"
diff --git a/config-model/src/test/derived/attributeprefetch/ilscripts.cfg b/config-model/src/test/derived/attributeprefetch/ilscripts.cfg
new file mode 100644
index 00000000000..201d529ce1d
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/ilscripts.cfg
@@ -0,0 +1,38 @@
+maxtermoccurrences 100
+ilscript[0].doctype "prefetch"
+ilscript[0].docfield[0] "singlebyte"
+ilscript[0].docfield[1] "multibyte"
+ilscript[0].docfield[2] "wsbyte"
+ilscript[0].docfield[3] "singleint"
+ilscript[0].docfield[4] "multiint"
+ilscript[0].docfield[5] "wsint"
+ilscript[0].docfield[6] "singlelong"
+ilscript[0].docfield[7] "multilong"
+ilscript[0].docfield[8] "wslong"
+ilscript[0].docfield[9] "singlefloat"
+ilscript[0].docfield[10] "multifloat"
+ilscript[0].docfield[11] "wsfloat"
+ilscript[0].docfield[12] "singledouble"
+ilscript[0].docfield[13] "multidouble"
+ilscript[0].docfield[14] "wsdouble"
+ilscript[0].docfield[15] "singlestring"
+ilscript[0].docfield[16] "multistring"
+ilscript[0].docfield[17] "wsstring"
+ilscript[0].content[0] "clear_state | guard { input singlebyte | attribute singlebyte; }"
+ilscript[0].content[1] "clear_state | guard { input multibyte | attribute multibyte; }"
+ilscript[0].content[2] "clear_state | guard { input wsbyte | attribute wsbyte; }"
+ilscript[0].content[3] "clear_state | guard { input singleint | attribute singleint; }"
+ilscript[0].content[4] "clear_state | guard { input multiint | attribute multiint; }"
+ilscript[0].content[5] "clear_state | guard { input wsint | attribute wsint; }"
+ilscript[0].content[6] "clear_state | guard { input singlelong | attribute singlelong; }"
+ilscript[0].content[7] "clear_state | guard { input multilong | attribute multilong; }"
+ilscript[0].content[8] "clear_state | guard { input wslong | attribute wslong; }"
+ilscript[0].content[9] "clear_state | guard { input singlefloat | attribute singlefloat; }"
+ilscript[0].content[10] "clear_state | guard { input multifloat | attribute multifloat; }"
+ilscript[0].content[11] "clear_state | guard { input wsfloat | attribute wsfloat; }"
+ilscript[0].content[12] "clear_state | guard { input singledouble | attribute singledouble; }"
+ilscript[0].content[13] "clear_state | guard { input multidouble | attribute multidouble; }"
+ilscript[0].content[14] "clear_state | guard { input wsdouble | attribute wsdouble; }"
+ilscript[0].content[15] "clear_state | guard { input singlestring | attribute singlestring; }"
+ilscript[0].content[16] "clear_state | guard { input multistring | attribute multistring; }"
+ilscript[0].content[17] "clear_state | guard { input wsstring | attribute wsstring; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributeprefetch/index-info.cfg b/config-model/src/test/derived/attributeprefetch/index-info.cfg
new file mode 100644
index 00000000000..526195dffb9
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/index-info.cfg
@@ -0,0 +1,115 @@
+indexinfo[0].name "prefetch"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "singlebyte"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "singlebyte"
+indexinfo[0].command[3].command "attribute"
+indexinfo[0].command[4].indexname "singlebyte"
+indexinfo[0].command[4].command "numerical"
+indexinfo[0].command[5].indexname "multibyte"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "multibyte"
+indexinfo[0].command[6].command "multivalue"
+indexinfo[0].command[7].indexname "multibyte"
+indexinfo[0].command[7].command "attribute"
+indexinfo[0].command[8].indexname "wsbyte"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "wsbyte"
+indexinfo[0].command[9].command "multivalue"
+indexinfo[0].command[10].indexname "wsbyte"
+indexinfo[0].command[10].command "attribute"
+indexinfo[0].command[11].indexname "singleint"
+indexinfo[0].command[11].command "index"
+indexinfo[0].command[12].indexname "singleint"
+indexinfo[0].command[12].command "attribute"
+indexinfo[0].command[13].indexname "singleint"
+indexinfo[0].command[13].command "numerical"
+indexinfo[0].command[14].indexname "multiint"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "multiint"
+indexinfo[0].command[15].command "multivalue"
+indexinfo[0].command[16].indexname "multiint"
+indexinfo[0].command[16].command "attribute"
+indexinfo[0].command[17].indexname "wsint"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "wsint"
+indexinfo[0].command[18].command "multivalue"
+indexinfo[0].command[19].indexname "wsint"
+indexinfo[0].command[19].command "attribute"
+indexinfo[0].command[20].indexname "singlelong"
+indexinfo[0].command[20].command "index"
+indexinfo[0].command[21].indexname "singlelong"
+indexinfo[0].command[21].command "attribute"
+indexinfo[0].command[22].indexname "singlelong"
+indexinfo[0].command[22].command "numerical"
+indexinfo[0].command[23].indexname "multilong"
+indexinfo[0].command[23].command "index"
+indexinfo[0].command[24].indexname "multilong"
+indexinfo[0].command[24].command "multivalue"
+indexinfo[0].command[25].indexname "multilong"
+indexinfo[0].command[25].command "attribute"
+indexinfo[0].command[26].indexname "wslong"
+indexinfo[0].command[26].command "index"
+indexinfo[0].command[27].indexname "wslong"
+indexinfo[0].command[27].command "multivalue"
+indexinfo[0].command[28].indexname "wslong"
+indexinfo[0].command[28].command "attribute"
+indexinfo[0].command[29].indexname "singlefloat"
+indexinfo[0].command[29].command "index"
+indexinfo[0].command[30].indexname "singlefloat"
+indexinfo[0].command[30].command "attribute"
+indexinfo[0].command[31].indexname "singlefloat"
+indexinfo[0].command[31].command "numerical"
+indexinfo[0].command[32].indexname "multifloat"
+indexinfo[0].command[32].command "index"
+indexinfo[0].command[33].indexname "multifloat"
+indexinfo[0].command[33].command "multivalue"
+indexinfo[0].command[34].indexname "multifloat"
+indexinfo[0].command[34].command "attribute"
+indexinfo[0].command[35].indexname "wsfloat"
+indexinfo[0].command[35].command "index"
+indexinfo[0].command[36].indexname "wsfloat"
+indexinfo[0].command[36].command "multivalue"
+indexinfo[0].command[37].indexname "wsfloat"
+indexinfo[0].command[37].command "attribute"
+indexinfo[0].command[38].indexname "singledouble"
+indexinfo[0].command[38].command "index"
+indexinfo[0].command[39].indexname "singledouble"
+indexinfo[0].command[39].command "attribute"
+indexinfo[0].command[40].indexname "singledouble"
+indexinfo[0].command[40].command "numerical"
+indexinfo[0].command[41].indexname "multidouble"
+indexinfo[0].command[41].command "index"
+indexinfo[0].command[42].indexname "multidouble"
+indexinfo[0].command[42].command "multivalue"
+indexinfo[0].command[43].indexname "multidouble"
+indexinfo[0].command[43].command "attribute"
+indexinfo[0].command[44].indexname "wsdouble"
+indexinfo[0].command[44].command "index"
+indexinfo[0].command[45].indexname "wsdouble"
+indexinfo[0].command[45].command "multivalue"
+indexinfo[0].command[46].indexname "wsdouble"
+indexinfo[0].command[46].command "attribute"
+indexinfo[0].command[47].indexname "singlestring"
+indexinfo[0].command[47].command "index"
+indexinfo[0].command[48].indexname "singlestring"
+indexinfo[0].command[48].command "attribute"
+indexinfo[0].command[49].indexname "multistring"
+indexinfo[0].command[49].command "index"
+indexinfo[0].command[50].indexname "multistring"
+indexinfo[0].command[50].command "multivalue"
+indexinfo[0].command[51].indexname "multistring"
+indexinfo[0].command[51].command "attribute"
+indexinfo[0].command[52].indexname "wsstring"
+indexinfo[0].command[52].command "index"
+indexinfo[0].command[53].indexname "wsstring"
+indexinfo[0].command[53].command "multivalue"
+indexinfo[0].command[54].indexname "wsstring"
+indexinfo[0].command[54].command "attribute"
+indexinfo[0].command[55].indexname "rankfeatures"
+indexinfo[0].command[55].command "index"
+indexinfo[0].command[56].indexname "summaryfeatures"
+indexinfo[0].command[56].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributeprefetch/rank-profiles.cfg b/config-model/src/test/derived/attributeprefetch/rank-profiles.cfg
new file mode 100644
index 00000000000..caca83a9a91
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/rank-profiles.cfg
@@ -0,0 +1,10 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributeprefetch/summary.cfg b/config-model/src/test/derived/attributeprefetch/summary.cfg
new file mode 100644
index 00000000000..ef01911a41f
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/summary.cfg
@@ -0,0 +1,51 @@
+defaultsummaryid 1151071433
+classes[0].id 1151071433
+classes[0].name "default"
+classes[0].fields[0].name "rankfeatures"
+classes[0].fields[0].type "featuredata"
+classes[0].fields[1].name "summaryfeatures"
+classes[0].fields[1].type "featuredata"
+classes[0].fields[2].name "documentid"
+classes[0].fields[2].type "longstring"
+classes[1].id 1279606967
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "singlebyte"
+classes[1].fields[0].type "byte"
+classes[1].fields[1].name "multibyte"
+classes[1].fields[1].type "jsonstring"
+classes[1].fields[2].name "wsbyte"
+classes[1].fields[2].type "jsonstring"
+classes[1].fields[3].name "singleint"
+classes[1].fields[3].type "integer"
+classes[1].fields[4].name "multiint"
+classes[1].fields[4].type "jsonstring"
+classes[1].fields[5].name "wsint"
+classes[1].fields[5].type "jsonstring"
+classes[1].fields[6].name "singlelong"
+classes[1].fields[6].type "int64"
+classes[1].fields[7].name "multilong"
+classes[1].fields[7].type "jsonstring"
+classes[1].fields[8].name "wslong"
+classes[1].fields[8].type "jsonstring"
+classes[1].fields[9].name "singlefloat"
+classes[1].fields[9].type "float"
+classes[1].fields[10].name "multifloat"
+classes[1].fields[10].type "jsonstring"
+classes[1].fields[11].name "wsfloat"
+classes[1].fields[11].type "jsonstring"
+classes[1].fields[12].name "singledouble"
+classes[1].fields[12].type "double"
+classes[1].fields[13].name "multidouble"
+classes[1].fields[13].type "jsonstring"
+classes[1].fields[14].name "wsdouble"
+classes[1].fields[14].type "jsonstring"
+classes[1].fields[15].name "singlestring"
+classes[1].fields[15].type "longstring"
+classes[1].fields[16].name "multistring"
+classes[1].fields[16].type "jsonstring"
+classes[1].fields[17].name "wsstring"
+classes[1].fields[17].type "jsonstring"
+classes[1].fields[18].name "rankfeatures"
+classes[1].fields[18].type "featuredata"
+classes[1].fields[19].name "summaryfeatures"
+classes[1].fields[19].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributeprefetch/summarymap.cfg b/config-model/src/test/derived/attributeprefetch/summarymap.cfg
new file mode 100644
index 00000000000..e42657eb2bb
--- /dev/null
+++ b/config-model/src/test/derived/attributeprefetch/summarymap.cfg
@@ -0,0 +1,61 @@
+defaultoutputclass -1
+override[0].field "rankfeatures"
+override[0].command "rankfeatures"
+override[0].arguments ""
+override[1].field "summaryfeatures"
+override[1].command "summaryfeatures"
+override[1].arguments ""
+override[2].field "singlebyte"
+override[2].command "attribute"
+override[2].arguments "singlebyte"
+override[3].field "multibyte"
+override[3].command "attribute"
+override[3].arguments "multibyte"
+override[4].field "wsbyte"
+override[4].command "attribute"
+override[4].arguments "wsbyte"
+override[5].field "singleint"
+override[5].command "attribute"
+override[5].arguments "singleint"
+override[6].field "multiint"
+override[6].command "attribute"
+override[6].arguments "multiint"
+override[7].field "wsint"
+override[7].command "attribute"
+override[7].arguments "wsint"
+override[8].field "singlelong"
+override[8].command "attribute"
+override[8].arguments "singlelong"
+override[9].field "multilong"
+override[9].command "attribute"
+override[9].arguments "multilong"
+override[10].field "wslong"
+override[10].command "attribute"
+override[10].arguments "wslong"
+override[11].field "singlefloat"
+override[11].command "attribute"
+override[11].arguments "singlefloat"
+override[12].field "multifloat"
+override[12].command "attribute"
+override[12].arguments "multifloat"
+override[13].field "wsfloat"
+override[13].command "attribute"
+override[13].arguments "wsfloat"
+override[14].field "singledouble"
+override[14].command "attribute"
+override[14].arguments "singledouble"
+override[15].field "multidouble"
+override[15].command "attribute"
+override[15].arguments "multidouble"
+override[16].field "wsdouble"
+override[16].command "attribute"
+override[16].arguments "wsdouble"
+override[17].field "singlestring"
+override[17].command "attribute"
+override[17].arguments "singlestring"
+override[18].field "multistring"
+override[18].command "attribute"
+override[18].arguments "multistring"
+override[19].field "wsstring"
+override[19].command "attribute"
+override[19].arguments "wsstring" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributerank/attributerank.sd b/config-model/src/test/derived/attributerank/attributerank.sd
new file mode 100644
index 00000000000..d20f9972404
--- /dev/null
+++ b/config-model/src/test/derived/attributerank/attributerank.sd
@@ -0,0 +1,41 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search attributerank {
+
+ document attributerank {
+
+ field singlebyte type byte {
+ indexing: attribute
+ }
+ field singleint type int {
+ indexing: attribute
+ }
+ field singlelong type long {
+ indexing: attribute
+ }
+ field singlefloat type float {
+ indexing: attribute
+ rank-type: identity
+ }
+ field singledouble type double {
+ indexing: attribute
+ rank-type: identity
+ }
+ field singlestring type string {
+ indexing: attribute
+ rank-type: identity
+ }
+ }
+
+ rank-profile firstprofile inherits default {
+ rank-type singlestring: default
+ }
+
+ rank-profile secondprofile inherits identity {
+ rank-type singlebyte: identity
+ rank-type singleint: identity
+ rank-type singlelong: identity
+ rank-type singlefloat: identity
+ rank-type singledouble: identity
+ rank-type singlestring: identity
+ }
+}
diff --git a/config-model/src/test/derived/attributerank/rank-profiles.cfg b/config-model/src/test/derived/attributerank/rank-profiles.cfg
new file mode 100644
index 00000000000..b37289705ec
--- /dev/null
+++ b/config-model/src/test/derived/attributerank/rank-profiles.cfg
@@ -0,0 +1,34 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "nativeAttributeMatch.weightTable.singlefloat"
+rankprofile[0].fef.property[0].value "linear(1,0)"
+rankprofile[0].fef.property[1].name "nativeAttributeMatch.weightTable.singledouble"
+rankprofile[0].fef.property[1].value "linear(1,0)"
+rankprofile[0].fef.property[2].name "nativeAttributeMatch.weightTable.singlestring"
+rankprofile[0].fef.property[2].value "linear(1,0)"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[2].name "firstprofile"
+rankprofile[2].fef.property[0].name "nativeAttributeMatch.weightTable.singlefloat"
+rankprofile[2].fef.property[0].value "linear(1,0)"
+rankprofile[2].fef.property[1].name "nativeAttributeMatch.weightTable.singledouble"
+rankprofile[2].fef.property[1].value "linear(1,0)"
+rankprofile[3].name "secondprofile"
+rankprofile[3].fef.property[0].name "nativeAttributeMatch.weightTable.singlebyte"
+rankprofile[3].fef.property[0].value "linear(1,0)"
+rankprofile[3].fef.property[1].name "nativeAttributeMatch.weightTable.singleint"
+rankprofile[3].fef.property[1].value "linear(1,0)"
+rankprofile[3].fef.property[2].name "nativeAttributeMatch.weightTable.singlelong"
+rankprofile[3].fef.property[2].value "linear(1,0)"
+rankprofile[3].fef.property[3].name "nativeAttributeMatch.weightTable.singlefloat"
+rankprofile[3].fef.property[3].value "linear(1,0)"
+rankprofile[3].fef.property[4].name "nativeAttributeMatch.weightTable.singledouble"
+rankprofile[3].fef.property[4].value "linear(1,0)"
+rankprofile[3].fef.property[5].name "nativeAttributeMatch.weightTable.singlestring"
+rankprofile[3].fef.property[5].value "linear(1,0)" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributes/attributes.cfg b/config-model/src/test/derived/attributes/attributes.cfg
new file mode 100644
index 00000000000..38f55f9645d
--- /dev/null
+++ b/config-model/src/test/derived/attributes/attributes.cfg
@@ -0,0 +1,342 @@
+attribute[0].name "a1"
+attribute[0].datatype STRING
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "a2"
+attribute[1].datatype STRING
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "a3"
+attribute[2].datatype STRING
+attribute[2].collectiontype SINGLE
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending true
+attribute[2].sortfunction UCA
+attribute[2].sortstrength PRIMARY
+attribute[2].sortlocale ""
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype ""
+attribute[3].name "a5"
+attribute[3].datatype STRING
+attribute[3].collectiontype SINGLE
+attribute[3].removeifzero false
+attribute[3].createifnonexistent false
+attribute[3].fastsearch false
+attribute[3].huge false
+attribute[3].sortascending true
+attribute[3].sortfunction UCA
+attribute[3].sortstrength PRIMARY
+attribute[3].sortlocale ""
+attribute[3].enablebitvectors false
+attribute[3].enableonlybitvector false
+attribute[3].fastaccess false
+attribute[3].arity 8
+attribute[3].lowerbound -9223372036854775808
+attribute[3].upperbound 9223372036854775807
+attribute[3].densepostinglistthreshold 0.4
+attribute[3].tensortype ""
+attribute[4].name "a6"
+attribute[4].datatype STRING
+attribute[4].collectiontype SINGLE
+attribute[4].removeifzero false
+attribute[4].createifnonexistent false
+attribute[4].fastsearch false
+attribute[4].huge false
+attribute[4].sortascending true
+attribute[4].sortfunction UCA
+attribute[4].sortstrength PRIMARY
+attribute[4].sortlocale ""
+attribute[4].enablebitvectors false
+attribute[4].enableonlybitvector false
+attribute[4].fastaccess false
+attribute[4].arity 8
+attribute[4].lowerbound -9223372036854775808
+attribute[4].upperbound 9223372036854775807
+attribute[4].densepostinglistthreshold 0.4
+attribute[4].tensortype ""
+attribute[5].name "b1"
+attribute[5].datatype STRING
+attribute[5].collectiontype SINGLE
+attribute[5].removeifzero false
+attribute[5].createifnonexistent false
+attribute[5].fastsearch false
+attribute[5].huge false
+attribute[5].sortascending true
+attribute[5].sortfunction UCA
+attribute[5].sortstrength PRIMARY
+attribute[5].sortlocale ""
+attribute[5].enablebitvectors false
+attribute[5].enableonlybitvector false
+attribute[5].fastaccess false
+attribute[5].arity 8
+attribute[5].lowerbound -9223372036854775808
+attribute[5].upperbound 9223372036854775807
+attribute[5].densepostinglistthreshold 0.4
+attribute[5].tensortype ""
+attribute[6].name "b2"
+attribute[6].datatype STRING
+attribute[6].collectiontype SINGLE
+attribute[6].removeifzero false
+attribute[6].createifnonexistent false
+attribute[6].fastsearch false
+attribute[6].huge false
+attribute[6].sortascending true
+attribute[6].sortfunction UCA
+attribute[6].sortstrength PRIMARY
+attribute[6].sortlocale ""
+attribute[6].enablebitvectors false
+attribute[6].enableonlybitvector false
+attribute[6].fastaccess false
+attribute[6].arity 8
+attribute[6].lowerbound -9223372036854775808
+attribute[6].upperbound 9223372036854775807
+attribute[6].densepostinglistthreshold 0.4
+attribute[6].tensortype ""
+attribute[7].name "b3"
+attribute[7].datatype STRING
+attribute[7].collectiontype SINGLE
+attribute[7].removeifzero false
+attribute[7].createifnonexistent false
+attribute[7].fastsearch false
+attribute[7].huge false
+attribute[7].sortascending true
+attribute[7].sortfunction UCA
+attribute[7].sortstrength PRIMARY
+attribute[7].sortlocale ""
+attribute[7].enablebitvectors false
+attribute[7].enableonlybitvector false
+attribute[7].fastaccess false
+attribute[7].arity 8
+attribute[7].lowerbound -9223372036854775808
+attribute[7].upperbound 9223372036854775807
+attribute[7].densepostinglistthreshold 0.4
+attribute[7].tensortype ""
+attribute[8].name "b4"
+attribute[8].datatype INT32
+attribute[8].collectiontype SINGLE
+attribute[8].removeifzero false
+attribute[8].createifnonexistent false
+attribute[8].fastsearch false
+attribute[8].huge false
+attribute[8].sortascending true
+attribute[8].sortfunction UCA
+attribute[8].sortstrength PRIMARY
+attribute[8].sortlocale ""
+attribute[8].enablebitvectors false
+attribute[8].enableonlybitvector false
+attribute[8].fastaccess false
+attribute[8].arity 8
+attribute[8].lowerbound -9223372036854775808
+attribute[8].upperbound 9223372036854775807
+attribute[8].densepostinglistthreshold 0.4
+attribute[8].tensortype ""
+attribute[9].name "b5"
+attribute[9].datatype INT32
+attribute[9].collectiontype SINGLE
+attribute[9].removeifzero false
+attribute[9].createifnonexistent false
+attribute[9].fastsearch false
+attribute[9].huge false
+attribute[9].sortascending true
+attribute[9].sortfunction UCA
+attribute[9].sortstrength PRIMARY
+attribute[9].sortlocale ""
+attribute[9].enablebitvectors false
+attribute[9].enableonlybitvector false
+attribute[9].fastaccess false
+attribute[9].arity 8
+attribute[9].lowerbound -9223372036854775808
+attribute[9].upperbound 9223372036854775807
+attribute[9].densepostinglistthreshold 0.4
+attribute[9].tensortype ""
+attribute[10].name "b6"
+attribute[10].datatype INT64
+attribute[10].collectiontype ARRAY
+attribute[10].removeifzero false
+attribute[10].createifnonexistent false
+attribute[10].fastsearch false
+attribute[10].huge false
+attribute[10].sortascending true
+attribute[10].sortfunction UCA
+attribute[10].sortstrength PRIMARY
+attribute[10].sortlocale ""
+attribute[10].enablebitvectors false
+attribute[10].enableonlybitvector false
+attribute[10].fastaccess false
+attribute[10].arity 8
+attribute[10].lowerbound -9223372036854775808
+attribute[10].upperbound 9223372036854775807
+attribute[10].densepostinglistthreshold 0.4
+attribute[10].tensortype ""
+attribute[11].name "b7"
+attribute[11].datatype DOUBLE
+attribute[11].collectiontype WEIGHTEDSET
+attribute[11].removeifzero false
+attribute[11].createifnonexistent false
+attribute[11].fastsearch false
+attribute[11].huge false
+attribute[11].sortascending true
+attribute[11].sortfunction UCA
+attribute[11].sortstrength PRIMARY
+attribute[11].sortlocale ""
+attribute[11].enablebitvectors false
+attribute[11].enableonlybitvector false
+attribute[11].fastaccess false
+attribute[11].arity 8
+attribute[11].lowerbound -9223372036854775808
+attribute[11].upperbound 9223372036854775807
+attribute[11].densepostinglistthreshold 0.4
+attribute[11].tensortype ""
+attribute[12].name "a9"
+attribute[12].datatype INT32
+attribute[12].collectiontype SINGLE
+attribute[12].removeifzero false
+attribute[12].createifnonexistent false
+attribute[12].fastsearch false
+attribute[12].huge false
+attribute[12].sortascending true
+attribute[12].sortfunction UCA
+attribute[12].sortstrength PRIMARY
+attribute[12].sortlocale ""
+attribute[12].enablebitvectors true
+attribute[12].enableonlybitvector false
+attribute[12].fastaccess false
+attribute[12].arity 8
+attribute[12].lowerbound -9223372036854775808
+attribute[12].upperbound 9223372036854775807
+attribute[12].densepostinglistthreshold 0.4
+attribute[12].tensortype ""
+attribute[13].name "a10"
+attribute[13].datatype INT32
+attribute[13].collectiontype ARRAY
+attribute[13].removeifzero false
+attribute[13].createifnonexistent false
+attribute[13].fastsearch true
+attribute[13].huge false
+attribute[13].sortascending true
+attribute[13].sortfunction UCA
+attribute[13].sortstrength PRIMARY
+attribute[13].sortlocale ""
+attribute[13].enablebitvectors true
+attribute[13].enableonlybitvector true
+attribute[13].fastaccess false
+attribute[13].arity 8
+attribute[13].lowerbound -9223372036854775808
+attribute[13].upperbound 9223372036854775807
+attribute[13].densepostinglistthreshold 0.4
+attribute[13].tensortype ""
+attribute[14].name "a11"
+attribute[14].datatype INT32
+attribute[14].collectiontype SINGLE
+attribute[14].removeifzero false
+attribute[14].createifnonexistent false
+attribute[14].fastsearch false
+attribute[14].huge false
+attribute[14].sortascending true
+attribute[14].sortfunction UCA
+attribute[14].sortstrength PRIMARY
+attribute[14].sortlocale ""
+attribute[14].enablebitvectors false
+attribute[14].enableonlybitvector false
+attribute[14].fastaccess true
+attribute[14].arity 8
+attribute[14].lowerbound -9223372036854775808
+attribute[14].upperbound 9223372036854775807
+attribute[14].densepostinglistthreshold 0.4
+attribute[14].tensortype ""
+attribute[15].name "a12"
+attribute[15].datatype INT32
+attribute[15].collectiontype SINGLE
+attribute[15].removeifzero false
+attribute[15].createifnonexistent false
+attribute[15].fastsearch false
+attribute[15].huge false
+attribute[15].sortascending true
+attribute[15].sortfunction UCA
+attribute[15].sortstrength PRIMARY
+attribute[15].sortlocale ""
+attribute[15].enablebitvectors true
+attribute[15].enableonlybitvector true
+attribute[15].fastaccess false
+attribute[15].arity 8
+attribute[15].lowerbound -9223372036854775808
+attribute[15].upperbound 9223372036854775807
+attribute[15].densepostinglistthreshold 0.4
+attribute[15].tensortype ""
+attribute[16].name "a7_arr"
+attribute[16].datatype STRING
+attribute[16].collectiontype ARRAY
+attribute[16].removeifzero false
+attribute[16].createifnonexistent false
+attribute[16].fastsearch false
+attribute[16].huge false
+attribute[16].sortascending true
+attribute[16].sortfunction UCA
+attribute[16].sortstrength PRIMARY
+attribute[16].sortlocale ""
+attribute[16].enablebitvectors false
+attribute[16].enableonlybitvector false
+attribute[16].fastaccess false
+attribute[16].arity 8
+attribute[16].lowerbound -9223372036854775808
+attribute[16].upperbound 9223372036854775807
+attribute[16].densepostinglistthreshold 0.4
+attribute[16].tensortype ""
+attribute[17].name "a8_arr"
+attribute[17].datatype STRING
+attribute[17].collectiontype ARRAY
+attribute[17].removeifzero false
+attribute[17].createifnonexistent false
+attribute[17].fastsearch false
+attribute[17].huge false
+attribute[17].sortascending true
+attribute[17].sortfunction UCA
+attribute[17].sortstrength PRIMARY
+attribute[17].sortlocale ""
+attribute[17].enablebitvectors false
+attribute[17].enableonlybitvector false
+attribute[17].fastaccess false
+attribute[17].arity 8
+attribute[17].lowerbound -9223372036854775808
+attribute[17].upperbound 9223372036854775807
+attribute[17].densepostinglistthreshold 0.4
+attribute[17].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributes/attributes.sd b/config-model/src/test/derived/attributes/attributes.sd
new file mode 100644
index 00000000000..95df6367ee7
--- /dev/null
+++ b/config-model/src/test/derived/attributes/attributes.sd
@@ -0,0 +1,121 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search attributes {
+
+ document attributes {
+
+ # Summary value taken from attribute
+ field a1 type string {
+ indexing: attribute | summary
+ match {
+ token
+ }
+ }
+
+ # Summary value taken from summary
+ field a2 type string {
+ indexing: attribute | summary
+ match {
+ token
+ }
+ }
+
+ # Summary value taken from summary
+ field a3 type string {
+ indexing: attribute | summary
+ match {
+ token
+ }
+ }
+
+ # No attribute
+ field a4 type string {
+ indexing: summary
+ }
+
+ # Unique only - not searchable. No match-group since the attribute is not to be used for searching
+ field a5 type string {
+ indexing: attribute | summary | index
+ }
+
+ # Not searchable
+ field a6 type string {
+ indexing: attribute | summary | index
+ }
+
+ # Multivalued; summary override
+ field a7 type string {
+ indexing: summary
+ }
+
+ # Multivalued; summary override
+ field a8 type string {
+ indexing: summary
+ }
+
+ field b1 type string {
+ indexing: attribute | summary
+ }
+
+ field b2 type string {
+ indexing: attribute | summary | index
+ }
+
+ field b3 type string {
+ indexing: attribute | summary | index
+ alias: date
+ }
+
+ field b4 type int {
+ indexing: attribute | summary
+ }
+
+ # integers (should not trigger exact-match query parsing)
+ field b5 type int {
+ indexing: summary | attribute
+ }
+ field b6 type array<long> {
+ indexing: summary | attribute
+ }
+ field b7 type weightedset<double> {
+ indexing: summary | attribute
+ }
+
+ field a9 type int {
+ indexing: attribute
+ attribute: enable-bit-vectors
+ }
+
+ field a10 type array<int> {
+ indexing: attribute
+ attribute {
+ fast-search
+ enable-only-bit-vector
+ enable-bit-vectors
+ }
+ }
+
+ field a11 type int {
+ indexing: attribute
+ attribute: fast-access
+ }
+
+ field a12 type int {
+ indexing: attribute
+ rank: filter
+ }
+
+ }
+
+ field a7_arr type array<string> {
+ indexing: input a7 | split ";" | attribute
+ }
+
+ field a8_arr type array<string> {
+ indexing: input a8 | split ";" | attribute
+ }
+
+ fieldset default {
+ fields: a5, a6
+ }
+
+}
diff --git a/config-model/src/test/derived/attributes/ilscripts.cfg b/config-model/src/test/derived/attributes/ilscripts.cfg
new file mode 100644
index 00000000000..450cc8e499c
--- /dev/null
+++ b/config-model/src/test/derived/attributes/ilscripts.cfg
@@ -0,0 +1,42 @@
+maxtermoccurrences 100
+ilscript[0].doctype "attributes"
+ilscript[0].docfield[0] "a1"
+ilscript[0].docfield[1] "a2"
+ilscript[0].docfield[2] "a3"
+ilscript[0].docfield[3] "a4"
+ilscript[0].docfield[4] "a5"
+ilscript[0].docfield[5] "a6"
+ilscript[0].docfield[6] "a7"
+ilscript[0].docfield[7] "a8"
+ilscript[0].docfield[8] "b1"
+ilscript[0].docfield[9] "b2"
+ilscript[0].docfield[10] "b3"
+ilscript[0].docfield[11] "b4"
+ilscript[0].docfield[12] "b5"
+ilscript[0].docfield[13] "b6"
+ilscript[0].docfield[14] "b7"
+ilscript[0].docfield[15] "a9"
+ilscript[0].docfield[16] "a10"
+ilscript[0].docfield[17] "a11"
+ilscript[0].docfield[18] "a12"
+ilscript[0].content[0] "clear_state | guard { input a7 | split \";\" | attribute a7_arr; }"
+ilscript[0].content[1] "clear_state | guard { input a8 | split \";\" | attribute a8_arr; }"
+ilscript[0].content[2] "clear_state | guard { input a1 | attribute a1 | summary a1; }"
+ilscript[0].content[3] "clear_state | guard { input a2 | attribute a2 | summary a2; }"
+ilscript[0].content[4] "clear_state | guard { input a3 | attribute a3 | summary a3; }"
+ilscript[0].content[5] "clear_state | guard { input a4 | summary a4; }"
+ilscript[0].content[6] "clear_state | guard { input a5 | tokenize normalize stem:\"SHORTEST\" | attribute a5 | summary a5 | index a5; }"
+ilscript[0].content[7] "clear_state | guard { input a6 | tokenize normalize stem:\"SHORTEST\" | attribute a6 | summary a6 | index a6; }"
+ilscript[0].content[8] "clear_state | guard { input a7 | summary a7; }"
+ilscript[0].content[9] "clear_state | guard { input a8 | summary a8; }"
+ilscript[0].content[10] "clear_state | guard { input b1 | attribute b1 | summary b1; }"
+ilscript[0].content[11] "clear_state | guard { input b2 | tokenize normalize stem:\"SHORTEST\" | attribute b2 | summary b2 | index b2; }"
+ilscript[0].content[12] "clear_state | guard { input b3 | tokenize normalize stem:\"SHORTEST\" | attribute b3 | summary b3 | index b3; }"
+ilscript[0].content[13] "clear_state | guard { input b4 | attribute b4 | summary b4; }"
+ilscript[0].content[14] "clear_state | guard { input b5 | summary b5 | attribute b5; }"
+ilscript[0].content[15] "clear_state | guard { input b6 | summary b6 | attribute b6; }"
+ilscript[0].content[16] "clear_state | guard { input b7 | summary b7 | attribute b7; }"
+ilscript[0].content[17] "clear_state | guard { input a9 | attribute a9; }"
+ilscript[0].content[18] "clear_state | guard { input a10 | attribute a10; }"
+ilscript[0].content[19] "clear_state | guard { input a11 | attribute a11; }"
+ilscript[0].content[20] "clear_state | guard { input a12 | attribute a12; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributes/index-info.cfg b/config-model/src/test/derived/attributes/index-info.cfg
new file mode 100644
index 00000000000..c9a12da4479
--- /dev/null
+++ b/config-model/src/test/derived/attributes/index-info.cfg
@@ -0,0 +1,141 @@
+indexinfo[0].name "attributes"
+indexinfo[0].command[].indexname "sddocname"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "sddocname"
+indexinfo[0].command[].command "word"
+indexinfo[0].command[].indexname "a1"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a1"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a2"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a2"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a3"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a3"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a4"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a5"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a5"
+indexinfo[0].command[].command "lowercase"
+indexinfo[0].command[].indexname "a5"
+indexinfo[0].command[].command "stem:SHORTEST"
+indexinfo[0].command[].indexname "a5"
+indexinfo[0].command[].command "normalize"
+indexinfo[0].command[].indexname "a6"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a6"
+indexinfo[0].command[].command "lowercase"
+indexinfo[0].command[].indexname "a6"
+indexinfo[0].command[].command "stem:SHORTEST"
+indexinfo[0].command[].indexname "a6"
+indexinfo[0].command[].command "normalize"
+indexinfo[0].command[].indexname "default"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "default"
+indexinfo[0].command[].command "lowercase"
+indexinfo[0].command[].indexname "default"
+indexinfo[0].command[].command "stem:SHORTEST"
+indexinfo[0].command[].indexname "default"
+indexinfo[0].command[].command "normalize"
+indexinfo[0].command[].indexname "a7"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a8"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b1"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b1"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "b1"
+indexinfo[0].command[].command "word"
+indexinfo[0].command[].indexname "b2"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b2"
+indexinfo[0].command[].command "lowercase"
+indexinfo[0].command[].indexname "b2"
+indexinfo[0].command[].command "stem:SHORTEST"
+indexinfo[0].command[].indexname "b2"
+indexinfo[0].command[].command "normalize"
+indexinfo[0].command[].indexname "b3"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b3"
+indexinfo[0].command[].command "lowercase"
+indexinfo[0].command[].indexname "b3"
+indexinfo[0].command[].command "stem:SHORTEST"
+indexinfo[0].command[].indexname "b3"
+indexinfo[0].command[].command "normalize"
+indexinfo[0].command[].indexname "b4"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b4"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "b4"
+indexinfo[0].command[].command "numerical"
+indexinfo[0].command[].indexname "b5"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b5"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "b5"
+indexinfo[0].command[].command "numerical"
+indexinfo[0].command[].indexname "b6"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b6"
+indexinfo[0].command[].command "multivalue"
+indexinfo[0].command[].indexname "b6"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "b7"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "b7"
+indexinfo[0].command[].command "multivalue"
+indexinfo[0].command[].indexname "b7"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a9"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a9"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a9"
+indexinfo[0].command[].command "numerical"
+indexinfo[0].command[].indexname "a10"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a10"
+indexinfo[0].command[].command "multivalue"
+indexinfo[0].command[].indexname "a10"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a10"
+indexinfo[0].command[].command "fast-search"
+indexinfo[0].command[].indexname "a11"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a11"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a11"
+indexinfo[0].command[].command "numerical"
+indexinfo[0].command[].indexname "a12"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a12"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a12"
+indexinfo[0].command[].command "numerical"
+indexinfo[0].command[].indexname "a7_arr"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a7_arr"
+indexinfo[0].command[].command "multivalue"
+indexinfo[0].command[].indexname "a7_arr"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a7_arr"
+indexinfo[0].command[].command "word"
+indexinfo[0].command[].indexname "a8_arr"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "a8_arr"
+indexinfo[0].command[].command "multivalue"
+indexinfo[0].command[].indexname "a8_arr"
+indexinfo[0].command[].command "attribute"
+indexinfo[0].command[].indexname "a8_arr"
+indexinfo[0].command[].command "word"
+indexinfo[0].command[].indexname "rankfeatures"
+indexinfo[0].command[].command "index"
+indexinfo[0].command[].indexname "summaryfeatures"
+indexinfo[0].command[].command "index"
+indexinfo[0].alias[].alias "date"
+indexinfo[0].alias[].indexname "b3" \ No newline at end of file
diff --git a/config-model/src/test/derived/attributes/summarymap.cfg b/config-model/src/test/derived/attributes/summarymap.cfg
new file mode 100644
index 00000000000..f13a8ab0b82
--- /dev/null
+++ b/config-model/src/test/derived/attributes/summarymap.cfg
@@ -0,0 +1,52 @@
+defaultoutputclass -1
+override[0].field "a1"
+override[0].command "attribute"
+override[0].arguments "a1"
+override[1].field "a2"
+override[1].command "attribute"
+override[1].arguments "a2"
+override[2].field "a3"
+override[2].command "attribute"
+override[2].arguments "a3"
+override[3].field "a5"
+override[3].command "attribute"
+override[3].arguments "a5"
+override[4].field "a6"
+override[4].command "attribute"
+override[4].arguments "a6"
+override[5].field "b1"
+override[5].command "attribute"
+override[5].arguments "b1"
+override[6].field "b2"
+override[6].command "attribute"
+override[6].arguments "b2"
+override[7].field "b3"
+override[7].command "attribute"
+override[7].arguments "b3"
+override[8].field "b4"
+override[8].command "attribute"
+override[8].arguments "b4"
+override[9].field "b5"
+override[9].command "attribute"
+override[9].arguments "b5"
+override[10].field "b6"
+override[10].command "attribute"
+override[10].arguments "b6"
+override[11].field "b7"
+override[11].command "attribute"
+override[11].arguments "b7"
+override[12].field "rankfeatures"
+override[12].command "rankfeatures"
+override[12].arguments ""
+override[13].field "summaryfeatures"
+override[13].command "summaryfeatures"
+override[13].arguments ""
+override[14].field "a9"
+override[14].command "attribute"
+override[14].arguments "a9"
+override[15].field "a11"
+override[15].command "attribute"
+override[15].arguments "a11"
+override[16].field "a12"
+override[16].command "attribute"
+override[16].arguments "a12" \ No newline at end of file
diff --git a/config-model/src/test/derived/combinedattributeandindexsearch/combinedattributeandindexsearch.sd b/config-model/src/test/derived/combinedattributeandindexsearch/combinedattributeandindexsearch.sd
new file mode 100644
index 00000000000..a20b5d2cfcc
--- /dev/null
+++ b/config-model/src/test/derived/combinedattributeandindexsearch/combinedattributeandindexsearch.sd
@@ -0,0 +1,34 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search combinedattributeandindexsearch {
+
+ document combinedattributeandindexsearch {
+
+ field index1 type string {
+ indexing: index | summary
+ }
+
+ field index2 type string {
+ indexing: index
+ }
+
+ field attribute1 type string {
+ indexing: attribute | summary
+ match {
+ token
+ }
+ }
+
+ field attribute2 type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ }
+
+ fieldset default {
+ fields: index1, index2, attribute1, attribute2
+ }
+
+}
diff --git a/config-model/src/test/derived/combinedattributeandindexsearch/index-info.cfg b/config-model/src/test/derived/combinedattributeandindexsearch/index-info.cfg
new file mode 100644
index 00000000000..cd56d204e96
--- /dev/null
+++ b/config-model/src/test/derived/combinedattributeandindexsearch/index-info.cfg
@@ -0,0 +1,41 @@
+indexinfo[0].name "combinedattributeandindexsearch"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "index1"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "default"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "index1"
+indexinfo[0].command[4].command "lowercase"
+indexinfo[0].command[5].indexname "default"
+indexinfo[0].command[5].command "lowercase"
+indexinfo[0].command[6].indexname "index1"
+indexinfo[0].command[6].command "stem:SHORTEST"
+indexinfo[0].command[7].indexname "default"
+indexinfo[0].command[7].command "stem:SHORTEST"
+indexinfo[0].command[8].indexname "index1"
+indexinfo[0].command[8].command "normalize"
+indexinfo[0].command[9].indexname "default"
+indexinfo[0].command[9].command "normalize"
+indexinfo[0].command[10].indexname "attribute1"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "attribute1"
+indexinfo[0].command[11].command "attribute"
+indexinfo[0].command[12].indexname "attribute2"
+indexinfo[0].command[12].command "attribute"
+indexinfo[0].command[12].indexname "attribute2"
+indexinfo[0].command[12].command "index"
+indexinfo[0].command[14].indexname "rankfeatures"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "summaryfeatures"
+indexinfo[0].command[15].command "index"
+indexinfo[0].command[2].indexname "index2"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[4].indexname "index2"
+indexinfo[0].command[4].command "lowercase"
+indexinfo[0].command[6].indexname "index2"
+indexinfo[0].command[6].command "stem:SHORTEST"
+indexinfo[0].command[8].indexname "index2"
+indexinfo[0].command[8].command "normalize"
diff --git a/config-model/src/test/derived/complex/attributes.cfg b/config-model/src/test/derived/complex/attributes.cfg
new file mode 100644
index 00000000000..b6ae59b5c24
--- /dev/null
+++ b/config-model/src/test/derived/complex/attributes.cfg
@@ -0,0 +1,171 @@
+attribute[0].name "prefixenabled"
+attribute[0].datatype STRING
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge true
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "fleeting"
+attribute[1].datatype FLOAT
+attribute[1].collectiontype ARRAY
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "fleeting2"
+attribute[2].datatype FLOAT
+attribute[2].collectiontype SINGLE
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending true
+attribute[2].sortfunction UCA
+attribute[2].sortstrength PRIMARY
+attribute[2].sortlocale ""
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype ""
+attribute[3].name "foundat"
+attribute[3].datatype INT64
+attribute[3].collectiontype SINGLE
+attribute[3].removeifzero false
+attribute[3].createifnonexistent false
+attribute[3].fastsearch false
+attribute[3].huge false
+attribute[3].sortascending true
+attribute[3].sortfunction UCA
+attribute[3].sortstrength PRIMARY
+attribute[3].sortlocale ""
+attribute[3].enablebitvectors false
+attribute[3].enableonlybitvector false
+attribute[3].fastaccess false
+attribute[3].arity 8
+attribute[3].lowerbound -9223372036854775808
+attribute[3].upperbound 9223372036854775807
+attribute[3].densepostinglistthreshold 0.4
+attribute[3].tensortype ""
+attribute[4].name "collapseby"
+attribute[4].datatype INT32
+attribute[4].collectiontype SINGLE
+attribute[4].removeifzero false
+attribute[4].createifnonexistent false
+attribute[4].fastsearch false
+attribute[4].huge false
+attribute[4].sortascending true
+attribute[4].sortfunction UCA
+attribute[4].sortstrength PRIMARY
+attribute[4].sortlocale ""
+attribute[4].enablebitvectors false
+attribute[4].enableonlybitvector false
+attribute[4].fastaccess false
+attribute[4].arity 8
+attribute[4].lowerbound -9223372036854775808
+attribute[4].upperbound 9223372036854775807
+attribute[4].densepostinglistthreshold 0.4
+attribute[4].tensortype ""
+attribute[5].name "ts"
+attribute[5].datatype INT64
+attribute[5].collectiontype SINGLE
+attribute[5].removeifzero false
+attribute[5].createifnonexistent false
+attribute[5].fastsearch false
+attribute[5].huge false
+attribute[5].sortascending true
+attribute[5].sortfunction UCA
+attribute[5].sortstrength PRIMARY
+attribute[5].sortlocale ""
+attribute[5].enablebitvectors false
+attribute[5].enableonlybitvector false
+attribute[5].fastaccess false
+attribute[5].arity 8
+attribute[5].lowerbound -9223372036854775808
+attribute[5].upperbound 9223372036854775807
+attribute[5].densepostinglistthreshold 0.4
+attribute[5].tensortype ""
+attribute[6].name "combineda"
+attribute[6].datatype INT32
+attribute[6].collectiontype SINGLE
+attribute[6].removeifzero false
+attribute[6].createifnonexistent false
+attribute[6].fastsearch false
+attribute[6].huge false
+attribute[6].sortascending true
+attribute[6].sortfunction UCA
+attribute[6].sortstrength PRIMARY
+attribute[6].sortlocale ""
+attribute[6].enablebitvectors false
+attribute[6].enableonlybitvector false
+attribute[6].fastaccess false
+attribute[6].arity 8
+attribute[6].lowerbound -9223372036854775808
+attribute[6].upperbound 9223372036854775807
+attribute[6].densepostinglistthreshold 0.4
+attribute[6].tensortype ""
+attribute[7].name "year_arr"
+attribute[7].datatype INT32
+attribute[7].collectiontype ARRAY
+attribute[7].removeifzero false
+attribute[7].createifnonexistent false
+attribute[7].fastsearch false
+attribute[7].huge false
+attribute[7].sortascending true
+attribute[7].sortfunction UCA
+attribute[7].sortstrength PRIMARY
+attribute[7].sortlocale ""
+attribute[7].enablebitvectors false
+attribute[7].enableonlybitvector false
+attribute[7].fastaccess false
+attribute[7].arity 8
+attribute[7].lowerbound -9223372036854775808
+attribute[7].upperbound 9223372036854775807
+attribute[7].densepostinglistthreshold 0.4
+attribute[7].tensortype ""
+attribute[8].name "year_sub"
+attribute[8].datatype INT32
+attribute[8].collectiontype SINGLE
+attribute[8].removeifzero false
+attribute[8].createifnonexistent false
+attribute[8].fastsearch false
+attribute[8].huge false
+attribute[8].sortascending true
+attribute[8].sortfunction UCA
+attribute[8].sortstrength PRIMARY
+attribute[8].sortlocale ""
+attribute[8].enablebitvectors false
+attribute[8].enableonlybitvector false
+attribute[8].fastaccess false
+attribute[8].arity 8
+attribute[8].lowerbound -9223372036854775808
+attribute[8].upperbound 9223372036854775807
+attribute[8].densepostinglistthreshold 0.4
+attribute[8].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/complex/complex.sd b/config-model/src/test/derived/complex/complex.sd
new file mode 100644
index 00000000000..6dcce8689f8
--- /dev/null
+++ b/config-model/src/test/derived/complex/complex.sd
@@ -0,0 +1,150 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search complex {
+
+ document complex {
+
+ field title type string {
+ indexing: index | summary
+ index default: prefix
+ rank-type: about
+ query-command: some-custom-command # Some command handled in a qustom searcher
+ query-command: some-other
+ alias default: some.default
+ alias: titlez
+ match: token
+ }
+
+ field location type string {
+
+ }
+
+ field dyntitle type string {
+ indexing: summary
+ summary: dynamic
+ }
+
+ field special1 type string {
+ indexing: index
+ stemming: none
+ }
+
+ field special2 type string {
+ indexing: index
+ stemming: none
+ }
+
+ field special3 type string {
+ indexing: index
+ stemming: none
+ }
+
+ field prefixenabled type string {
+ indexing: index | attribute
+ index: prefix
+ attribute: prefetch
+ attribute: huge
+ normalizing: none
+ stemming: shortest
+ }
+
+ field source type uri {
+ indexing: summary | index
+ }
+
+ field docurl type uri {
+ indexing: index
+ alias docurl: url
+ }
+
+ field fleeting type array<float> {
+ indexing: index | attribute
+ attribute: prefetch
+ }
+
+ field fleeting2 type float {
+ indexing: index | attribute
+ attribute: prefetch
+ }
+
+ field foundat type long {
+ indexing: index
+ }
+
+ field collapseby type int {
+ indexing: index
+ }
+
+ field yEaR type int {
+
+ }
+
+ field stringfield type string {
+ indexing: summary | index
+ }
+
+ field exactemento type string {
+ indexing: index
+ }
+
+ field exactagain type string {
+ indexing: index
+ }
+
+ field ts type long {
+ indexing: attribute
+ }
+
+ field combineda type int {
+ indexing: index
+ }
+
+ field combinedb type string {
+ indexing: index
+ }
+
+ field category type string {
+
+ }
+
+ }
+
+ field woe type string {
+ indexing: input location | summary | index
+ }
+
+ field year_sub type int {
+ indexing: input yEaR - 1900 | attribute year_sub
+ }
+
+ field year_arr type array<int> {
+ indexing: input yEaR | to_array | attribute
+ }
+
+ # A field defined outside an index
+ field exact type string {
+ indexing {
+ input title . input category | summary | index;
+ }
+ stemming: none
+ normalizing: none
+ rank-type: identity
+ }
+
+ # Some experimental ranking changes
+ rank-profile experimental inherits default {
+ }
+
+ rank-profile other inherits experimental {
+ rank-type source: identity
+ }
+ fieldset default {
+ fields: title, stringfield
+ }
+ fieldset special {
+ fields: special1, special2, special3
+ }
+ fieldset all {
+ fields: combineda, combinedb
+ }
+
+}
diff --git a/config-model/src/test/derived/complex/documentmanager.cfg b/config-model/src/test/derived/complex/documentmanager.cfg
new file mode 100644
index 00000000000..66ebbbb2846
--- /dev/null
+++ b/config-model/src/test/derived/complex/documentmanager.cfg
@@ -0,0 +1,119 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 1650586661
+datatype[1].arraytype[0].datatype 1
+datatype[2].id -1245117006
+datatype[2].arraytype[0].datatype 0
+datatype[3].id -1749463923
+datatype[3].structtype[0].name "complex.header"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "title"
+datatype[3].structtype[0].field[0].datatype 2
+datatype[3].structtype[0].field[1].name "location"
+datatype[3].structtype[0].field[1].datatype 2
+datatype[3].structtype[0].field[2].name "dyntitle"
+datatype[3].structtype[0].field[2].datatype 2
+datatype[3].structtype[0].field[3].name "special1"
+datatype[3].structtype[0].field[3].datatype 2
+datatype[3].structtype[0].field[4].name "special2"
+datatype[3].structtype[0].field[4].datatype 2
+datatype[3].structtype[0].field[5].name "special3"
+datatype[3].structtype[0].field[5].datatype 2
+datatype[3].structtype[0].field[6].name "prefixenabled"
+datatype[3].structtype[0].field[6].datatype 2
+datatype[3].structtype[0].field[7].name "source"
+datatype[3].structtype[0].field[7].datatype 10
+datatype[3].structtype[0].field[8].name "docurl"
+datatype[3].structtype[0].field[8].datatype 10
+datatype[3].structtype[0].field[9].name "fleeting"
+datatype[3].structtype[0].field[9].datatype 1650586661
+datatype[3].structtype[0].field[10].name "fleeting2"
+datatype[3].structtype[0].field[10].datatype 1
+datatype[3].structtype[0].field[11].name "foundat"
+datatype[3].structtype[0].field[11].datatype 4
+datatype[3].structtype[0].field[12].name "collapseby"
+datatype[3].structtype[0].field[12].datatype 0
+datatype[3].structtype[0].field[13].name "yEaR"
+datatype[3].structtype[0].field[13].datatype 0
+datatype[3].structtype[0].field[14].name "stringfield"
+datatype[3].structtype[0].field[14].datatype 2
+datatype[3].structtype[0].field[15].name "exactemento"
+datatype[3].structtype[0].field[15].datatype 2
+datatype[3].structtype[0].field[16].name "exactagain"
+datatype[3].structtype[0].field[16].datatype 2
+datatype[3].structtype[0].field[17].name "ts"
+datatype[3].structtype[0].field[17].datatype 4
+datatype[3].structtype[0].field[18].name "combineda"
+datatype[3].structtype[0].field[18].datatype 0
+datatype[3].structtype[0].field[19].name "combinedb"
+datatype[3].structtype[0].field[19].datatype 2
+datatype[3].structtype[0].field[20].name "category"
+datatype[3].structtype[0].field[20].datatype 2
+datatype[3].structtype[0].field[21].name "woe"
+datatype[3].structtype[0].field[21].datatype 2
+datatype[3].structtype[0].field[22].name "year_sub"
+datatype[3].structtype[0].field[22].datatype 0
+datatype[3].structtype[0].field[23].name "year_arr"
+datatype[3].structtype[0].field[23].datatype -1245117006
+datatype[3].structtype[0].field[24].name "exact"
+datatype[3].structtype[0].field[24].datatype 2
+datatype[3].structtype[0].field[25].name "rankfeatures"
+datatype[3].structtype[0].field[25].datatype 2
+datatype[3].structtype[0].field[26].name "summaryfeatures"
+datatype[3].structtype[0].field[26].datatype 2
+datatype[4].id -1665926686
+datatype[4].structtype[0].name "complex.body"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[5].id -1402929550
+datatype[5].documenttype[0].name "complex"
+datatype[5].documenttype[0].version 0
+datatype[5].documenttype[0].inherits[0].name "document"
+datatype[5].documenttype[0].inherits[0].version 0
+datatype[5].documenttype[0].headerstruct -1749463923
+datatype[5].documenttype[0].bodystruct -1665926686
+datatype[5].documenttype[0].fieldsets{default}.fields[0] "stringfield"
+datatype[5].documenttype[0].fieldsets{default}.fields[1] "title"
+datatype[5].documenttype[0].fieldsets{special}.fields[0] "special1"
+datatype[5].documenttype[0].fieldsets{special}.fields[1] "special2"
+datatype[5].documenttype[0].fieldsets{special}.fields[2] "special3"
+datatype[5].documenttype[0].fieldsets{all}.fields[0] "combineda"
+datatype[5].documenttype[0].fieldsets{all}.fields[1] "combinedb"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[0] "category"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[1] "collapseby"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[2] "combineda"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[3] "combinedb"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[4] "docurl"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[5] "dyntitle"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[6] "exactagain"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[7] "exactemento"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[8] "fleeting"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[9] "fleeting2"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[10] "foundat"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[11] "location"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[12] "prefixenabled"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[13] "source"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[14] "special1"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[15] "special2"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[16] "special3"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[17] "stringfield"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[18] "title"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[19] "ts"
+datatype[5].documenttype[0].fieldsets{[document]}.fields[20] "yEaR"
diff --git a/config-model/src/test/derived/complex/ilscripts.cfg b/config-model/src/test/derived/complex/ilscripts.cfg
new file mode 100644
index 00000000000..05de728e16e
--- /dev/null
+++ b/config-model/src/test/derived/complex/ilscripts.cfg
@@ -0,0 +1,48 @@
+maxtermoccurrences 100
+ilscript[0].doctype "complex"
+ilscript[0].docfield[0] "title"
+ilscript[0].docfield[1] "location"
+ilscript[0].docfield[2] "dyntitle"
+ilscript[0].docfield[3] "special1"
+ilscript[0].docfield[4] "special2"
+ilscript[0].docfield[5] "special3"
+ilscript[0].docfield[6] "prefixenabled"
+ilscript[0].docfield[7] "source"
+ilscript[0].docfield[8] "docurl"
+ilscript[0].docfield[9] "fleeting"
+ilscript[0].docfield[10] "fleeting2"
+ilscript[0].docfield[11] "foundat"
+ilscript[0].docfield[12] "collapseby"
+ilscript[0].docfield[13] "yEaR"
+ilscript[0].docfield[14] "stringfield"
+ilscript[0].docfield[15] "exactemento"
+ilscript[0].docfield[16] "exactagain"
+ilscript[0].docfield[17] "ts"
+ilscript[0].docfield[18] "combineda"
+ilscript[0].docfield[19] "combinedb"
+ilscript[0].docfield[20] "category"
+ilscript[0].content[0] "clear_state | guard { input title . input category | tokenize | summary exact | index exact; }"
+ilscript[0].content[1] "clear_state | guard { input location | tokenize normalize stem:\"SHORTEST\" | summary woe | index woe; }"
+ilscript[0].content[2] "clear_state | guard { input yEaR | to_array | attribute year_arr; }"
+ilscript[0].content[3] "clear_state | guard { input yEaR - 1900 | attribute year_sub; }"
+ilscript[0].content[4] "clear_state | guard { input title | tokenize normalize stem:\"SHORTEST\" | index title | summary title; }"
+ilscript[0].content[5] "clear_state | guard { input dyntitle | tokenize normalize stem:\"SHORTEST\" | summary dyntitle; }"
+ilscript[0].content[6] "clear_state | guard { input special1 | tokenize normalize | index special1; }"
+ilscript[0].content[7] "clear_state | guard { input special2 | tokenize normalize | index special2; }"
+ilscript[0].content[8] "clear_state | guard { input special3 | tokenize normalize | index special3; }"
+ilscript[0].content[9] "clear_state | guard { input prefixenabled | tokenize stem:\"SHORTEST\" | index prefixenabled | attribute prefixenabled; }"
+ilscript[0].content[10] "clear_state | guard { input source | summary source | index source; }"
+ilscript[0].content[11] "clear_state | guard { input docurl | index docurl; }"
+ilscript[0].content[12] "clear_state | guard { input fleeting | attribute fleeting; }"
+ilscript[0].content[13] "clear_state | guard { input fleeting2 | attribute fleeting2; }"
+ilscript[0].content[14] "clear_state | guard { input foundat | attribute foundat; }"
+ilscript[0].content[15] "clear_state | guard { input collapseby | attribute collapseby; }"
+ilscript[0].content[16] "clear_state | guard { input stringfield | tokenize normalize stem:\"SHORTEST\" | summary stringfield | index stringfield; }"
+ilscript[0].content[17] "clear_state | guard { input exactemento | tokenize normalize stem:\"SHORTEST\" | index exactemento; }"
+ilscript[0].content[18] "clear_state | guard { input exactagain | tokenize normalize stem:\"SHORTEST\" | index exactagain; }"
+ilscript[0].content[19] "clear_state | guard { input ts | attribute ts; }"
+ilscript[0].content[20] "clear_state | guard { input combineda | attribute combineda; }"
+ilscript[0].content[21] "clear_state | guard { input combinedb | tokenize normalize stem:\"SHORTEST\" | index combinedb; }"
+ilscript[0].content[22] "input category | passthrough category"
+ilscript[0].content[23] "input location | passthrough location"
+ilscript[0].content[24] "input yEaR | passthrough yEaR" \ No newline at end of file
diff --git a/config-model/src/test/derived/complex/rank-profiles.cfg b/config-model/src/test/derived/complex/rank-profiles.cfg
new file mode 100644
index 00000000000..5e2804d0ee6
--- /dev/null
+++ b/config-model/src/test/derived/complex/rank-profiles.cfg
@@ -0,0 +1,68 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.exact"
+rankprofile[0].fef.property[0].value "expdecay(100,12.50)"
+rankprofile[0].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.exact"
+rankprofile[0].fef.property[1].value "loggrowth(1500,4000,19)"
+rankprofile[0].fef.property[2].name "nativeProximity.proximityTable.exact"
+rankprofile[0].fef.property[2].value "expdecay(5000,3)"
+rankprofile[0].fef.property[3].name "nativeProximity.reverseProximityTable.exact"
+rankprofile[0].fef.property[3].value "expdecay(3000,3)"
+rankprofile[0].fef.property[4].name "nativeFieldMatch.firstOccurrenceTable.title"
+rankprofile[0].fef.property[4].value "expdecay(8000,12.50)"
+rankprofile[0].fef.property[5].name "nativeFieldMatch.occurrenceCountTable.title"
+rankprofile[0].fef.property[5].value "loggrowth(1500,4000,19)"
+rankprofile[0].fef.property[6].name "nativeProximity.proximityTable.title"
+rankprofile[0].fef.property[6].value "expdecay(500,3)"
+rankprofile[0].fef.property[7].name "nativeProximity.reverseProximityTable.title"
+rankprofile[0].fef.property[7].value "expdecay(400,3)"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[2].name "experimental"
+rankprofile[2].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.exact"
+rankprofile[2].fef.property[0].value "expdecay(100,12.50)"
+rankprofile[2].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.exact"
+rankprofile[2].fef.property[1].value "loggrowth(1500,4000,19)"
+rankprofile[2].fef.property[2].name "nativeProximity.proximityTable.exact"
+rankprofile[2].fef.property[2].value "expdecay(5000,3)"
+rankprofile[2].fef.property[3].name "nativeProximity.reverseProximityTable.exact"
+rankprofile[2].fef.property[3].value "expdecay(3000,3)"
+rankprofile[2].fef.property[4].name "nativeFieldMatch.firstOccurrenceTable.title"
+rankprofile[2].fef.property[4].value "expdecay(8000,12.50)"
+rankprofile[2].fef.property[5].name "nativeFieldMatch.occurrenceCountTable.title"
+rankprofile[2].fef.property[5].value "loggrowth(1500,4000,19)"
+rankprofile[2].fef.property[6].name "nativeProximity.proximityTable.title"
+rankprofile[2].fef.property[6].value "expdecay(500,3)"
+rankprofile[2].fef.property[7].name "nativeProximity.reverseProximityTable.title"
+rankprofile[2].fef.property[7].value "expdecay(400,3)"
+rankprofile[3].name "other"
+rankprofile[3].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.source"
+rankprofile[3].fef.property[0].value "expdecay(100,12.50)"
+rankprofile[3].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.source"
+rankprofile[3].fef.property[1].value "loggrowth(1500,4000,19)"
+rankprofile[3].fef.property[2].name "nativeProximity.proximityTable.source"
+rankprofile[3].fef.property[2].value "expdecay(5000,3)"
+rankprofile[3].fef.property[3].name "nativeProximity.reverseProximityTable.source"
+rankprofile[3].fef.property[3].value "expdecay(3000,3)"
+rankprofile[3].fef.property[4].name "nativeFieldMatch.firstOccurrenceTable.exact"
+rankprofile[3].fef.property[4].value "expdecay(100,12.50)"
+rankprofile[3].fef.property[5].name "nativeFieldMatch.occurrenceCountTable.exact"
+rankprofile[3].fef.property[5].value "loggrowth(1500,4000,19)"
+rankprofile[3].fef.property[6].name "nativeProximity.proximityTable.exact"
+rankprofile[3].fef.property[6].value "expdecay(5000,3)"
+rankprofile[3].fef.property[7].name "nativeProximity.reverseProximityTable.exact"
+rankprofile[3].fef.property[7].value "expdecay(3000,3)"
+rankprofile[3].fef.property[8].name "nativeFieldMatch.firstOccurrenceTable.title"
+rankprofile[3].fef.property[8].value "expdecay(8000,12.50)"
+rankprofile[3].fef.property[9].name "nativeFieldMatch.occurrenceCountTable.title"
+rankprofile[3].fef.property[9].value "loggrowth(1500,4000,19)"
+rankprofile[3].fef.property[10].name "nativeProximity.proximityTable.title"
+rankprofile[3].fef.property[10].value "expdecay(500,3)"
+rankprofile[3].fef.property[11].name "nativeProximity.reverseProximityTable.title"
+rankprofile[3].fef.property[11].value "expdecay(400,3)"
diff --git a/config-model/src/test/derived/complex/summary.cfg b/config-model/src/test/derived/complex/summary.cfg
new file mode 100644
index 00000000000..1cf6ad457b3
--- /dev/null
+++ b/config-model/src/test/derived/complex/summary.cfg
@@ -0,0 +1,43 @@
+defaultsummaryid 1506848752
+classes[0].id 1506848752
+classes[0].name "default"
+classes[0].fields[0].name "woe"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "exact"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "title"
+classes[0].fields[2].type "longstring"
+classes[0].fields[3].name "dyntitle"
+classes[0].fields[3].type "longstring"
+classes[0].fields[4].name "source"
+classes[0].fields[4].type "longstring"
+classes[0].fields[5].name "stringfield"
+classes[0].fields[5].type "longstring"
+classes[0].fields[6].name "rankfeatures"
+classes[0].fields[6].type "featuredata"
+classes[0].fields[7].name "summaryfeatures"
+classes[0].fields[7].type "featuredata"
+classes[0].fields[8].name "documentid"
+classes[0].fields[8].type "longstring"
+classes[1].id 28214929
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "year_sub"
+classes[1].fields[0].type "integer"
+classes[1].fields[1].name "prefixenabled"
+classes[1].fields[1].type "longstring"
+classes[1].fields[2].name "fleeting"
+classes[1].fields[2].type "jsonstring"
+classes[1].fields[3].name "fleeting2"
+classes[1].fields[3].type "float"
+classes[1].fields[4].name "foundat"
+classes[1].fields[4].type "int64"
+classes[1].fields[5].name "collapseby"
+classes[1].fields[5].type "integer"
+classes[1].fields[6].name "ts"
+classes[1].fields[6].type "int64"
+classes[1].fields[7].name "combineda"
+classes[1].fields[7].type "integer"
+classes[1].fields[8].name "rankfeatures"
+classes[1].fields[8].type "featuredata"
+classes[1].fields[9].name "summaryfeatures"
+classes[1].fields[9].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/complex/summarymap.cfg b/config-model/src/test/derived/complex/summarymap.cfg
new file mode 100644
index 00000000000..37721347b1c
--- /dev/null
+++ b/config-model/src/test/derived/complex/summarymap.cfg
@@ -0,0 +1,34 @@
+defaultoutputclass -1
+override[0].field "dyntitle"
+override[0].command "dynamicteaser"
+override[0].arguments "dyntitle"
+override[1].field "rankfeatures"
+override[1].command "rankfeatures"
+override[1].arguments ""
+override[2].field "summaryfeatures"
+override[2].command "summaryfeatures"
+override[2].arguments ""
+override[3].field "year_sub"
+override[3].command "attribute"
+override[3].arguments "year_sub"
+override[4].field "prefixenabled"
+override[4].command "attribute"
+override[4].arguments "prefixenabled"
+override[5].field "fleeting"
+override[5].command "attribute"
+override[5].arguments "fleeting"
+override[6].field "fleeting2"
+override[6].command "attribute"
+override[6].arguments "fleeting2"
+override[7].field "foundat"
+override[7].command "attribute"
+override[7].arguments "foundat"
+override[8].field "collapseby"
+override[8].command "attribute"
+override[8].arguments "collapseby"
+override[9].field "ts"
+override[9].command "attribute"
+override[9].arguments "ts"
+override[10].field "combineda"
+override[10].command "attribute"
+override[10].arguments "combineda" \ No newline at end of file
diff --git a/config-model/src/test/derived/deriver/child.sd b/config-model/src/test/derived/deriver/child.sd
new file mode 100644
index 00000000000..ba2f5637019
--- /dev/null
+++ b/config-model/src/test/derived/deriver/child.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+
+ document child inherits parent {
+
+ field a type string {
+ indexing: index | summary | attribute
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/deriver/grandparent.sd b/config-model/src/test/derived/deriver/grandparent.sd
new file mode 100644
index 00000000000..7247b2126f1
--- /dev/null
+++ b/config-model/src/test/derived/deriver/grandparent.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search grandparent {
+
+ document grandparent {
+
+ field c type string {
+ indexing: index
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/deriver/ilscripts.cfg b/config-model/src/test/derived/deriver/ilscripts.cfg
new file mode 100644
index 00000000000..749474dc350
--- /dev/null
+++ b/config-model/src/test/derived/deriver/ilscripts.cfg
@@ -0,0 +1,8 @@
+ilscript[1]
+ilscript[child].name "child"
+ilscript[child].doctype "child"
+ilscript[child].content[4]
+ilscript[child].content[0] "\"child\" | index sddocname | summary sddocname"
+ilscript[child].content[1] "input c | tokenize normalize stem:\"SHORTEST\" | index c"
+ilscript[child].content[2] "input b | tokenize normalize stem:\"SHORTEST\" | index b | summary b"
+ilscript[child].content[3] "input a | tokenize normalize stem:\"SHORTEST\" | index a | attribute a"
diff --git a/config-model/src/test/derived/deriver/parent.sd b/config-model/src/test/derived/deriver/parent.sd
new file mode 100644
index 00000000000..57a3438cb7d
--- /dev/null
+++ b/config-model/src/test/derived/deriver/parent.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search parent {
+
+ document parent inherits grandparent {
+
+ field b type string {
+ indexing: index | summary
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/documentderiver/compression_body.sd b/config-model/src/test/derived/documentderiver/compression_body.sd
new file mode 100644
index 00000000000..f70dcce9c72
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/compression_body.sd
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search compressed_body {
+
+ document compressed_body {
+ body {
+ compression {
+ level:6
+ }
+ }
+
+ field from type string {
+ }
+
+ field content type string {
+ body
+ }
+ }
+
+}
+
diff --git a/config-model/src/test/derived/documentderiver/compression_both.sd b/config-model/src/test/derived/documentderiver/compression_both.sd
new file mode 100644
index 00000000000..5c93f41b9a3
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/compression_both.sd
@@ -0,0 +1,26 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search compressed_both {
+
+ document compressed_both {
+ compression {
+ threshold:90
+ level:9
+ }
+
+ header {
+ compression {
+ threshold:50
+ level:6
+ }
+ }
+
+ field from type string {
+ }
+
+ field content type string {
+ body
+ }
+ }
+
+}
+
diff --git a/config-model/src/test/derived/documentderiver/compression_header.sd b/config-model/src/test/derived/documentderiver/compression_header.sd
new file mode 100644
index 00000000000..8eddbd0aa0d
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/compression_header.sd
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search compressed_header {
+
+ document compressed_header {
+ header {
+ compression {
+ level:9
+ }
+ }
+
+ field from type string {
+ }
+
+ field content type string {
+ body
+ }
+ }
+
+}
+
diff --git a/config-model/src/test/derived/documentderiver/documentmanager.cfg b/config-model/src/test/derived/documentderiver/documentmanager.cfg
new file mode 100644
index 00000000000..d56d90ba358
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/documentmanager.cfg
@@ -0,0 +1,319 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -843666531
+datatype[1].structtype[0].name "compressed_body.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "from"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id 1704844530
+datatype[2].structtype[0].name "compressed_body.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype LZ4
+datatype[2].structtype[0].compresslevel 6
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 0
+datatype[2].structtype[0].field[0].name "content"
+datatype[2].structtype[0].field[0].datatype 2
+datatype[3].id 1417245026
+datatype[3].documenttype[0].name "compressed_body"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct -843666531
+datatype[3].documenttype[0].bodystruct 1704844530
+datatype[4].id -484354914
+datatype[4].structtype[0].name "compressed_both.header"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype LZ4
+datatype[4].structtype[0].compresslevel 6
+datatype[4].structtype[0].compressthreshold 50
+datatype[4].structtype[0].compressminsize 0
+datatype[4].structtype[0].field[0].name "from"
+datatype[4].structtype[0].field[0].datatype 2
+datatype[5].id -1007627725
+datatype[5].structtype[0].name "compressed_both.body"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype LZ4
+datatype[5].structtype[0].compresslevel 9
+datatype[5].structtype[0].compressthreshold 90
+datatype[5].structtype[0].compressminsize 0
+datatype[5].structtype[0].field[0].name "content"
+datatype[5].structtype[0].field[0].datatype 2
+datatype[6].id 1417705345
+datatype[6].documenttype[0].name "compressed_both"
+datatype[6].documenttype[0].version 0
+datatype[6].documenttype[0].inherits[0].name "document"
+datatype[6].documenttype[0].inherits[0].version 0
+datatype[6].documenttype[0].headerstruct -484354914
+datatype[6].documenttype[0].bodystruct -1007627725
+datatype[7].id -940182894
+datatype[7].structtype[0].name "compressed_header.header"
+datatype[7].structtype[0].version 0
+datatype[7].structtype[0].compresstype LZ4
+datatype[7].structtype[0].compresslevel 9
+datatype[7].structtype[0].compressthreshold 95
+datatype[7].structtype[0].compressminsize 0
+datatype[7].structtype[0].field[0].name "from"
+datatype[7].structtype[0].field[0].datatype 2
+datatype[8].id -579052249
+datatype[8].structtype[0].name "compressed_header.body"
+datatype[8].structtype[0].version 0
+datatype[8].structtype[0].compresstype NONE
+datatype[8].structtype[0].compresslevel 0
+datatype[8].structtype[0].compressthreshold 95
+datatype[8].structtype[0].compressminsize 800
+datatype[8].structtype[0].field[0].name "content"
+datatype[8].structtype[0].field[0].datatype 2
+datatype[9].id 1946084365
+datatype[9].documenttype[0].name "compressed_header"
+datatype[9].documenttype[0].version 0
+datatype[9].documenttype[0].inherits[0].name "document"
+datatype[9].documenttype[0].inherits[0].version 0
+datatype[9].documenttype[0].headerstruct -940182894
+datatype[9].documenttype[0].bodystruct -579052249
+datatype[10].id -88808602
+datatype[10].structtype[0].name "mail.header"
+datatype[10].structtype[0].version 0
+datatype[10].structtype[0].compresstype NONE
+datatype[10].structtype[0].compresslevel 0
+datatype[10].structtype[0].compressthreshold 95
+datatype[10].structtype[0].compressminsize 800
+datatype[10].structtype[0].field[0].name "URI"
+datatype[10].structtype[0].field[0].datatype 10
+datatype[10].structtype[0].field[1].name "mailid"
+datatype[10].structtype[0].field[1].datatype 2
+datatype[10].structtype[0].field[2].name "date"
+datatype[10].structtype[0].field[2].datatype 0
+datatype[10].structtype[0].field[3].name "from"
+datatype[10].structtype[0].field[3].datatype 2
+datatype[10].structtype[0].field[4].name "replyto"
+datatype[10].structtype[0].field[4].datatype 3
+datatype[10].structtype[0].field[5].name "to"
+datatype[10].structtype[0].field[5].datatype 2
+datatype[10].structtype[0].field[6].name "cc"
+datatype[10].structtype[0].field[6].datatype 2
+datatype[10].structtype[0].field[7].name "bcc"
+datatype[10].structtype[0].field[7].datatype 2
+datatype[10].structtype[0].field[8].name "subject"
+datatype[10].structtype[0].field[8].datatype 2
+datatype[11].id -1244861287
+datatype[11].arraytype[0].datatype 3
+datatype[12].id -953584901
+datatype[12].structtype[0].name "mail.body"
+datatype[12].structtype[0].version 0
+datatype[12].structtype[0].compresstype NONE
+datatype[12].structtype[0].compresslevel 0
+datatype[12].structtype[0].compressthreshold 95
+datatype[12].structtype[0].compressminsize 800
+datatype[12].structtype[0].field[0].name "mailbody"
+datatype[12].structtype[0].field[0].datatype 3
+datatype[12].structtype[0].field[1].name "attachmentcount"
+datatype[12].structtype[0].field[1].datatype 0
+datatype[12].structtype[0].field[2].name "attachmentnames"
+datatype[12].structtype[0].field[2].datatype 2
+datatype[12].structtype[0].field[3].name "attachmenttypes"
+datatype[12].structtype[0].field[3].datatype 2
+datatype[12].structtype[0].field[4].name "attachmentlanguages"
+datatype[12].structtype[0].field[4].datatype 2
+datatype[12].structtype[0].field[5].name "attachmentcontent"
+datatype[12].structtype[0].field[5].datatype 2
+datatype[12].structtype[0].field[6].name "attachments"
+datatype[12].structtype[0].field[6].datatype -1244861287
+datatype[13].id -1081574983
+datatype[13].documenttype[0].name "mail"
+datatype[13].documenttype[0].version 0
+datatype[13].documenttype[0].inherits[0].name "document"
+datatype[13].documenttype[0].inherits[0].version 0
+datatype[13].documenttype[0].headerstruct -88808602
+datatype[13].documenttype[0].bodystruct -953584901
+datatype[14].id -1486737430
+datatype[14].arraytype[0].datatype 2
+datatype[15].id 519906144
+datatype[15].weightedsettype[0].datatype 0
+datatype[15].weightedsettype[0].createifnonexistant false
+datatype[15].weightedsettype[0].removeifzero false
+datatype[16].id 363959257
+datatype[16].weightedsettype[0].datatype 0
+datatype[16].weightedsettype[0].createifnonexistant true
+datatype[16].weightedsettype[0].removeifzero true
+datatype[17].id -1910204744
+datatype[17].structtype[0].name "music.header"
+datatype[17].structtype[0].version 0
+datatype[17].structtype[0].compresstype NONE
+datatype[17].structtype[0].compresslevel 0
+datatype[17].structtype[0].compressthreshold 95
+datatype[17].structtype[0].compressminsize 800
+datatype[17].structtype[0].field[0].name "url"
+datatype[17].structtype[0].field[0].datatype 10
+datatype[17].structtype[0].field[1].name "title"
+datatype[17].structtype[0].field[1].datatype 2
+datatype[17].structtype[0].field[2].name "artist"
+datatype[17].structtype[0].field[2].datatype 2
+datatype[17].structtype[0].field[3].name "year"
+datatype[17].structtype[0].field[3].datatype 0
+datatype[17].structtype[0].field[4].name "description"
+datatype[17].structtype[0].field[4].datatype 3
+datatype[17].structtype[0].field[5].name "tracks"
+datatype[17].structtype[0].field[5].datatype -1486737430
+datatype[17].structtype[0].field[6].name "popularity"
+datatype[17].structtype[0].field[6].datatype 519906144
+datatype[17].structtype[0].field[7].name "popularity2"
+datatype[17].structtype[0].field[7].datatype 363959257
+datatype[17].structtype[0].field[8].name "popularity3"
+datatype[17].structtype[0].field[8].datatype 363959257
+datatype[18].id 993120973
+datatype[18].structtype[0].name "music.body"
+datatype[18].structtype[0].version 0
+datatype[18].structtype[0].compresstype NONE
+datatype[18].structtype[0].compresslevel 0
+datatype[18].structtype[0].compressthreshold 95
+datatype[18].structtype[0].compressminsize 800
+datatype[19].id 1412693671
+datatype[19].documenttype[0].name "music"
+datatype[19].documenttype[0].version 0
+datatype[19].documenttype[0].inherits[0].name "document"
+datatype[19].documenttype[0].inherits[0].version 0
+datatype[19].documenttype[0].headerstruct -1910204744
+datatype[19].documenttype[0].bodystruct 993120973
+datatype[20].id 2006483754
+datatype[20].structtype[0].name "newssummary.header"
+datatype[20].structtype[0].version 0
+datatype[20].structtype[0].compresstype NONE
+datatype[20].structtype[0].compresslevel 0
+datatype[20].structtype[0].compressthreshold 95
+datatype[20].structtype[0].compressminsize 800
+datatype[20].structtype[0].field[0].name "title"
+datatype[20].structtype[0].field[0].datatype 2
+datatype[20].structtype[0].field[1].name "abstract"
+datatype[20].structtype[0].field[1].datatype 2
+datatype[20].structtype[0].field[2].name "sourcename"
+datatype[20].structtype[0].field[2].datatype 2
+datatype[20].structtype[0].field[3].name "providername"
+datatype[20].structtype[0].field[3].datatype 2
+datatype[20].structtype[0].field[4].name "thumburl"
+datatype[20].structtype[0].field[4].datatype 2
+datatype[20].structtype[0].field[5].name "thumbwidth"
+datatype[20].structtype[0].field[5].datatype 0
+datatype[20].structtype[0].field[6].name "thumbheight"
+datatype[20].structtype[0].field[6].datatype 0
+datatype[20].structtype[0].field[7].name "language"
+datatype[20].structtype[0].field[7].datatype 2
+datatype[20].structtype[0].field[8].name "crawldocid"
+datatype[20].structtype[0].field[8].datatype 2
+datatype[20].structtype[0].field[9].name "url"
+datatype[20].structtype[0].field[9].datatype 10
+datatype[20].structtype[0].field[10].name "sourceurl"
+datatype[20].structtype[0].field[10].datatype 10
+datatype[20].structtype[0].field[11].name "categories"
+datatype[20].structtype[0].field[11].datatype 2
+datatype[20].structtype[0].field[12].name "pubdate"
+datatype[20].structtype[0].field[12].datatype 4
+datatype[20].structtype[0].field[13].name "expdate"
+datatype[20].structtype[0].field[13].datatype 4
+datatype[20].structtype[0].field[14].name "fingerprint"
+datatype[20].structtype[0].field[14].datatype 0
+datatype[20].structtype[0].field[15].name "debug"
+datatype[20].structtype[0].field[15].datatype 2
+datatype[20].structtype[0].field[16].name "attributes"
+datatype[20].structtype[0].field[16].datatype 2
+datatype[20].structtype[0].field[17].name "searchcluster"
+datatype[20].structtype[0].field[17].datatype 2
+datatype[20].structtype[0].field[18].name "eustaticrank"
+datatype[20].structtype[0].field[18].datatype 0
+datatype[20].structtype[0].field[19].name "usstaticrank"
+datatype[20].structtype[0].field[19].datatype 0
+datatype[20].structtype[0].field[20].name "asiastaticrank"
+datatype[20].structtype[0].field[20].datatype 0
+datatype[21].id -2059783233
+datatype[21].structtype[0].name "newssummary.body"
+datatype[21].structtype[0].version 0
+datatype[21].structtype[0].compresstype NONE
+datatype[21].structtype[0].compresslevel 0
+datatype[21].structtype[0].compressthreshold 95
+datatype[21].structtype[0].compressminsize 800
+datatype[22].id -756330891
+datatype[22].documenttype[0].name "newssummary"
+datatype[22].documenttype[0].version 0
+datatype[22].documenttype[0].inherits[0].name "document"
+datatype[22].documenttype[0].inherits[0].version 0
+datatype[22].documenttype[0].headerstruct 2006483754
+datatype[22].documenttype[0].bodystruct -2059783233
+datatype[23].id 2098419674
+datatype[23].structtype[0].name "newsarticle.header"
+datatype[23].structtype[0].version 0
+datatype[23].structtype[0].compresstype NONE
+datatype[23].structtype[0].compresslevel 0
+datatype[23].structtype[0].compressthreshold 95
+datatype[23].structtype[0].compressminsize 800
+datatype[23].structtype[0].field[0].name "dynabstract"
+datatype[23].structtype[0].field[0].datatype 2
+datatype[23].structtype[0].field[1].name "othersourcenames"
+datatype[23].structtype[0].field[1].datatype 2
+datatype[23].structtype[0].field[2].name "author"
+datatype[23].structtype[0].field[2].datatype 2
+datatype[23].structtype[0].field[3].name "otherlanguages"
+datatype[23].structtype[0].field[3].datatype 2
+datatype[23].structtype[0].field[4].name "charset"
+datatype[23].structtype[0].field[4].datatype 2
+datatype[23].structtype[0].field[5].name "mimetype"
+datatype[23].structtype[0].field[5].datatype 2
+datatype[23].structtype[0].field[6].name "referrerurl"
+datatype[23].structtype[0].field[6].datatype 10
+datatype[23].structtype[0].field[7].name "sourcelocation"
+datatype[23].structtype[0].field[7].datatype 2
+datatype[23].structtype[0].field[8].name "sourcecountry"
+datatype[23].structtype[0].field[8].datatype 2
+datatype[23].structtype[0].field[9].name "sourcelocale"
+datatype[23].structtype[0].field[9].datatype 2
+datatype[23].structtype[0].field[10].name "sourcecontinent"
+datatype[23].structtype[0].field[10].datatype 2
+datatype[23].structtype[0].field[11].name "articlecountry"
+datatype[23].structtype[0].field[11].datatype 2
+datatype[23].structtype[0].field[12].name "articlelocale"
+datatype[23].structtype[0].field[12].datatype 2
+datatype[23].structtype[0].field[13].name "articlecontinent"
+datatype[23].structtype[0].field[13].datatype 2
+datatype[23].structtype[0].field[14].name "sourcerank"
+datatype[23].structtype[0].field[14].datatype 0
+datatype[23].structtype[0].field[15].name "crawldate"
+datatype[23].structtype[0].field[15].datatype 4
+datatype[23].structtype[0].field[16].name "indexdate"
+datatype[23].structtype[0].field[16].datatype 4
+datatype[23].structtype[0].field[17].name "procdate"
+datatype[23].structtype[0].field[17].datatype 4
+datatype[23].structtype[0].field[18].name "sourceid"
+datatype[23].structtype[0].field[18].datatype 0
+datatype[23].structtype[0].field[19].name "sourcefeedid"
+datatype[23].structtype[0].field[19].datatype 0
+datatype[24].id 197293167
+datatype[24].structtype[0].name "newsarticle.body"
+datatype[24].structtype[0].version 0
+datatype[24].structtype[0].compresstype NONE
+datatype[24].structtype[0].compresslevel 0
+datatype[24].structtype[0].compressthreshold 95
+datatype[24].structtype[0].compressminsize 800
+datatype[24].structtype[0].field[0].name "body"
+datatype[24].structtype[0].field[0].datatype 2
+datatype[25].id -1710661691
+datatype[25].documenttype[0].name "newsarticle"
+datatype[25].documenttype[0].version 0
+datatype[25].documenttype[0].inherits[0].name "document"
+datatype[25].documenttype[0].inherits[0].version 0
+datatype[25].documenttype[0].inherits[1].name "newssummary"
+datatype[25].documenttype[0].inherits[1].version 0
+datatype[25].documenttype[0].headerstruct 2098419674
+datatype[25].documenttype[0].bodystruct 197293167
diff --git a/config-model/src/test/derived/documentderiver/mail.sd b/config-model/src/test/derived/documentderiver/mail.sd
new file mode 100644
index 00000000000..77195a5273c
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/mail.sd
@@ -0,0 +1,112 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search mail {
+
+ stemming: none
+
+ document mail {
+
+ field URI type uri {
+ indexing: summary
+ summary-to: default, mailid
+ }
+
+ field mailid type string {
+ indexing: summary | index
+ match: prefix
+ summary-to: default, mailid
+ }
+
+ field date type int {
+ indexing: summary | attribute | index
+ match: prefix
+ }
+
+ field from type string {
+ indexing: summary | index
+ # index-to: from, sender, address, header, default, all
+ match: prefix
+ }
+
+ field replyto type raw {
+ indexing: summary | index
+ # index-to: replyto
+ match: prefix
+ }
+
+ field to type string {
+ indexing: summary | index
+ # index-to: to, recipient, address, header, default, all
+ match: prefix
+ }
+
+ field cc type string {
+ indexing: index
+ # index-to: cc, recipient, address, header, default, all
+ match: prefix
+ }
+
+ field bcc type string {
+ indexing: index
+ # index-to: bcc
+ match: prefix
+ }
+
+ field subject type string {
+ indexing: summary | index
+ # index-to: subject, header, default, all
+ match: prefix
+ }
+
+ field mailbody type raw {
+ indexing: summary | index
+ # index-to: mailbody, default, all
+ match: substring
+ body
+ }
+
+ field attachmentcount type int {
+ indexing: summary | index
+ body
+ }
+
+ field attachmentnames type string {
+ indexing: index
+ # index-to: attachmentname, all
+ body
+ }
+
+ field attachmenttypes type string {
+ indexing: index
+ # index-to: attachmenttype, all
+ body
+ }
+
+ field attachmentlanguages type string {
+ indexing: index
+ match: prefix
+ body
+ }
+
+ field attachmentcontent type string {
+ indexing: summary | index
+ # index-to: attachment, all
+ match: prefix
+ body
+ }
+
+ field attachments type raw[] {
+ body
+ }
+
+ }
+
+ document-summary default {
+ summary snippet type string {
+ dynamic
+ source: body, attachmentcontent
+ }
+
+ }
+
+}
+
diff --git a/config-model/src/test/derived/documentderiver/music.sd b/config-model/src/test/derived/documentderiver/music.sd
new file mode 100644
index 00000000000..83d5d648424
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/music.sd
@@ -0,0 +1,44 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document music {
+
+ # Link to album main page
+ field url type uri { }
+
+ # Title of album
+ field title type string { }
+
+ # Album artist
+ field artist type string { }
+
+ # Album production year
+ field year type int { }
+
+ # Album description - about the album
+ field description type raw { }
+
+ # Names of the album tracks
+ field tracks type array<string> { }
+
+ # How many have given this album the grade 0/1/2/3
+ field popularity type weightedset<int> { }
+
+ # How many have given this album the grade 0/1/2/3
+ field popularity2 type weightedset<int> {
+ weightedset: create-if-nonexistent
+ weightedset: remove-if-zero
+ }
+
+ # How many have given this album the grade 0/1/2/3
+ field popularity3 type weightedset<int> {
+ weightedset {
+ create-if-nonexistent
+ remove-if-zero
+ }
+ }
+
+ }
+
+}
+
diff --git a/config-model/src/test/derived/documentderiver/newsarticle.sd b/config-model/src/test/derived/documentderiver/newsarticle.sd
new file mode 100644
index 00000000000..9407eca0d18
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/newsarticle.sd
@@ -0,0 +1,126 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search newsarticle {
+
+ document newsarticle inherits newssummary {
+
+ field dynabstract type string {
+ indexing: summary
+ }
+
+ field body type string {
+ body
+ indexing: summary | index
+ # index-to: body, default
+ stemming: none
+ }
+
+ field othersourcenames type string {
+ indexing: summary | index
+ # index-to: othersourcenames, source
+ stemming: none
+ }
+
+ field author type string {
+ indexing: summary | index
+ stemming: none
+ }
+
+ field otherlanguages type string {
+ indexing: summary | index
+ # index-to: languages
+ stemming: none
+ }
+
+ field charset type string {
+ indexing: summary
+ stemming: none
+ }
+
+ field mimetype type string {
+ indexing: summary
+ stemming: none
+ }
+
+ field referrerurl type uri {
+ indexing: summary | lowercase | tokenize | index
+ stemming: none
+ }
+
+ field sourcelocation type string {
+ indexing: summary | index
+ stemming: none
+ alias: location
+ }
+
+ field sourcecountry type string {
+ indexing: summary | index
+ stemming: none
+ # index-to: sourcecountry, sourcelocation
+ }
+
+ field sourcelocale type string {
+ indexing: summary | index
+ stemming: none
+ # index-to: sourcelocale, sourcelocation
+ }
+
+ field sourcecontinent type string {
+ indexing: summary | index
+ stemming: none
+ # index-to: sourcecontinent, sourcelocation
+ }
+
+ field articlecountry type string {
+ indexing: summary | index
+ stemming: none
+ }
+
+ field articlelocale type string {
+ indexing: summary | index
+ stemming: none
+ }
+
+ field articlecontinent type string {
+ indexing: summary | index
+ stemming: none
+ }
+
+ field sourcerank type int {
+ indexing: summary | index | set_var tmpsourcerank
+ }
+
+ field crawldate type long {
+ indexing: summary | index
+ }
+
+ field indexdate type long {
+ indexing: now | summary | index
+ }
+
+ field procdate type long {
+ indexing: summary | index
+ }
+
+ field sourceid type int {
+ indexing: summary | index
+ }
+
+ field sourcefeedid type int {
+ indexing: summary | index
+ }
+
+ }
+
+ rank-profile date {
+ }
+
+ rank-profile usrank inherits default {
+ }
+
+ rank-profile eurank inherits default {
+ }
+
+ rank-profile asiarank inherits default {
+ }
+
+}
diff --git a/config-model/src/test/derived/documentderiver/newssummary.sd b/config-model/src/test/derived/documentderiver/newssummary.sd
new file mode 100644
index 00000000000..3492b95308e
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/newssummary.sd
@@ -0,0 +1,165 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search newssummary {
+
+ document newssummary {
+
+ field title type string {
+ indexing: summary | index
+ # index-to: title, titleabstract, default
+ stemming: none
+ alias: headline
+ }
+
+ field abstract type string {
+ indexing: summary | index
+ # index-to: abstract, titleabstract, default
+ stemming: none
+ }
+
+ field sourcename type string {
+ indexing: summary | index
+ # index-to: sourcename, source
+ stemming: none
+ }
+
+ field providername type string {
+ indexing: summary | index
+ # index-to: providername, source
+ stemming: none
+ alias: provider
+ }
+
+ field thumburl type string {
+ indexing: summary | lowercase | tokenize | index
+ stemming: none
+ }
+
+ field thumbwidth type int {
+ indexing: summary | index
+ }
+
+ field thumbheight type int {
+ indexing: summary | index
+ }
+
+ field language type string {
+ indexing: summary | index
+ # index-to: language, languages
+ stemming: none
+ }
+
+ field crawldocid type string {
+ indexing: summary
+ stemming: none
+ }
+
+ field url type uri {
+ indexing: summary | lowercase | tokenize | index
+ stemming: none
+ }
+
+ field sourceurl type uri {
+ indexing: summary | lowercase | tokenize | index
+ stemming: none
+ }
+
+ field categories type string {
+ indexing: summary | index
+ stemming: none
+ alias: category
+ alias: cat
+ }
+
+ field pubdate type long {
+ indexing: summary | index | attribute pubdate | set_var tmppubdate
+ alias: date
+ }
+
+ field expdate type long {
+ indexing: summary | index
+ }
+
+ field fingerprint type int {
+ indexing: summary | index
+ }
+
+ field debug type string {
+ indexing {
+
+ # Initialize variables used for superduper ranking
+ 0 | set_var superduperus | set_var superdupereu | set_var superduperasia;
+
+ input debug | lowercase | summary | normalize | tokenize | index;
+ input debug | lowercase | split ";" | for_each {
+ # Loop through each token in debug string
+ switch {
+ case "superduperus": 10 | set_var superduperus;
+ case "superdupereu": 10 | set_var superdupereu;
+ case "superduperasia": 10 | set_var superduperasia;
+ }
+ };
+ }
+ indexing-rewrite: none
+ stemming: none
+ }
+
+ field attributes type string {
+ indexing {
+
+ # Initialize variables used for superduper ranking
+ 1 | set_var superdupermod;
+
+ input attributes | lowercase | summary | normalize | tokenize | index;
+ input attributes | lowercase | split ";" | for_each {
+ # Loop through each token in attributes string
+ switch {
+
+ # De-rank PR articles using the following rules:
+ # 1. Set editedstaticrank to '1'
+ # 2. Subtract 2.5 hours (9000 seconds) from timestamp used in ranking
+ # 3. No superduper rank
+ case "typepr": 1 | set_var tmpsourcerank | get_var tmppubdate - 9000 | set_var tmppubdate | 0 | set_var superdupermod;
+ }
+ };
+ }
+ indexing-rewrite: none
+ stemming: none
+ }
+
+ field searchcluster type string {
+ indexing: summary
+ stemming: none
+ }
+
+ field eustaticrank type int {
+ indexing {
+ get_var tmpsourcerank * 4000 + get_var superdupereu * get_var superdupermod * 1000 + get_var tmppubdate * 0.5 | summary | index | attribute eustaticrank;
+ }
+ }
+
+ field usstaticrank type int {
+ indexing {
+ get_var tmpsourcerank * 4000 + get_var superduperus * get_var superdupermod * 1000 + get_var tmppubdate * 0.5 | summary | index | attribute usstaticrank;
+ }
+ }
+
+ field asiastaticrank type int {
+ indexing {
+ get_var tmpsourcerank * 4000 + get_var superduperasia * get_var superdupermod * 1000 + get_var tmppubdate * 0.5 | summary | index | attribute asiastaticrank;
+ }
+ }
+ }
+
+ rank-profile date {
+ }
+
+ rank-profile usrank inherits default {
+ }
+
+ rank-profile eurank inherits default {
+ }
+
+ rank-profile asiarank inherits default {
+ }
+
+}
diff --git a/config-model/src/test/derived/documentderiver/sombrero.sd b/config-model/src/test/derived/documentderiver/sombrero.sd
new file mode 100644
index 00000000000..c4f9e90c06c
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/sombrero.sd
@@ -0,0 +1,36 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search webdoc {
+ document webdoc {
+ #
+ # a simple key-value pair
+ #
+ struct keyvalue {
+ field key type string {}
+ field value type string {}
+ }
+
+ #
+ # tags have a name and an array of attributes
+ #
+ struct tagvalue {
+ field name type string {}
+ # todo: this should be a map of attributes, not an array
+ field attributes type array<keyvalue> {}
+ }
+
+ #
+ # wordforms are (kind, form, weight) triplets
+ # todo: "kind" should be an enum; check how enums are used.
+ #
+ struct wordform {
+ field kind type int {}
+ field form type string {}
+ field weight type float {}
+ }
+
+ #
+ # web documents have zero or more HTML source strings
+ #
+ field html type string {}
+ }
+}
diff --git a/config-model/src/test/derived/documentderiver/vsmfields.cfg b/config-model/src/test/derived/documentderiver/vsmfields.cfg
new file mode 100644
index 00000000000..34d4ac85315
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/vsmfields.cfg
@@ -0,0 +1,489 @@
+fieldspec[58]
+fieldspec[0].name sddocname
+fieldspec[0].searchmethod AUTOUTF8
+fieldspec[0].arg1 "exact"
+fieldspec[1].name title
+fieldspec[1].searchmethod AUTOUTF8
+fieldspec[1].arg1 ""
+fieldspec[2].name abstract
+fieldspec[2].searchmethod AUTOUTF8
+fieldspec[2].arg1 ""
+fieldspec[3].name sourcename
+fieldspec[3].searchmethod AUTOUTF8
+fieldspec[3].arg1 ""
+fieldspec[4].name providername
+fieldspec[4].searchmethod AUTOUTF8
+fieldspec[4].arg1 ""
+fieldspec[5].name thumburl
+fieldspec[5].searchmethod AUTOUTF8
+fieldspec[5].arg1 ""
+fieldspec[6].name thumbwidth
+fieldspec[6].searchmethod INT32
+fieldspec[6].arg1 ""
+fieldspec[7].name thumbheight
+fieldspec[7].searchmethod INT32
+fieldspec[7].arg1 ""
+fieldspec[8].name language
+fieldspec[8].searchmethod AUTOUTF8
+fieldspec[8].arg1 ""
+fieldspec[9].name crawldocid
+fieldspec[9].searchmethod AUTOUTF8
+fieldspec[9].arg1 ""
+fieldspec[10].name url
+fieldspec[10].searchmethod AUTOUTF8
+fieldspec[10].arg1 ""
+fieldspec[11].name sourceurl
+fieldspec[11].searchmethod AUTOUTF8
+fieldspec[11].arg1 ""
+fieldspec[12].name categories
+fieldspec[12].searchmethod AUTOUTF8
+fieldspec[12].arg1 ""
+fieldspec[13].name pubdate
+fieldspec[13].searchmethod INT64
+fieldspec[13].arg1 ""
+fieldspec[14].name expdate
+fieldspec[14].searchmethod INT64
+fieldspec[14].arg1 ""
+fieldspec[15].name fingerprint
+fieldspec[15].searchmethod INT32
+fieldspec[15].arg1 ""
+fieldspec[16].name debug
+fieldspec[16].searchmethod AUTOUTF8
+fieldspec[16].arg1 ""
+fieldspec[17].name attributes
+fieldspec[17].searchmethod AUTOUTF8
+fieldspec[17].arg1 ""
+fieldspec[18].name searchcluster
+fieldspec[18].searchmethod AUTOUTF8
+fieldspec[18].arg1 ""
+fieldspec[19].name eustaticrank
+fieldspec[19].searchmethod INT32
+fieldspec[19].arg1 ""
+fieldspec[20].name usstaticrank
+fieldspec[20].searchmethod INT32
+fieldspec[20].arg1 ""
+fieldspec[21].name asiastaticrank
+fieldspec[21].searchmethod INT32
+fieldspec[21].arg1 ""
+fieldspec[22].name dynabstract
+fieldspec[22].searchmethod AUTOUTF8
+fieldspec[22].arg1 ""
+fieldspec[23].name body
+fieldspec[23].searchmethod AUTOUTF8
+fieldspec[23].arg1 ""
+fieldspec[24].name othersourcenames
+fieldspec[24].searchmethod AUTOUTF8
+fieldspec[24].arg1 ""
+fieldspec[25].name author
+fieldspec[25].searchmethod AUTOUTF8
+fieldspec[25].arg1 ""
+fieldspec[26].name otherlanguages
+fieldspec[26].searchmethod AUTOUTF8
+fieldspec[26].arg1 ""
+fieldspec[27].name charset
+fieldspec[27].searchmethod AUTOUTF8
+fieldspec[27].arg1 ""
+fieldspec[28].name mimetype
+fieldspec[28].searchmethod AUTOUTF8
+fieldspec[28].arg1 ""
+fieldspec[29].name referrerurl
+fieldspec[29].searchmethod AUTOUTF8
+fieldspec[29].arg1 ""
+fieldspec[30].name sourcelocation
+fieldspec[30].searchmethod AUTOUTF8
+fieldspec[30].arg1 ""
+fieldspec[31].name sourcecountry
+fieldspec[31].searchmethod AUTOUTF8
+fieldspec[31].arg1 ""
+fieldspec[32].name sourcelocale
+fieldspec[32].searchmethod AUTOUTF8
+fieldspec[32].arg1 ""
+fieldspec[33].name sourcecontinent
+fieldspec[33].searchmethod AUTOUTF8
+fieldspec[33].arg1 ""
+fieldspec[34].name articlecountry
+fieldspec[34].searchmethod AUTOUTF8
+fieldspec[34].arg1 ""
+fieldspec[35].name articlelocale
+fieldspec[35].searchmethod AUTOUTF8
+fieldspec[35].arg1 ""
+fieldspec[36].name articlecontinent
+fieldspec[36].searchmethod AUTOUTF8
+fieldspec[36].arg1 ""
+fieldspec[37].name sourcerank
+fieldspec[37].searchmethod INT32
+fieldspec[37].arg1 ""
+fieldspec[38].name crawldate
+fieldspec[38].searchmethod INT64
+fieldspec[38].arg1 ""
+fieldspec[39].name indexdate
+fieldspec[39].searchmethod INT64
+fieldspec[39].arg1 ""
+fieldspec[40].name procdate
+fieldspec[40].searchmethod INT64
+fieldspec[40].arg1 ""
+fieldspec[41].name sourceid
+fieldspec[41].searchmethod INT32
+fieldspec[41].arg1 ""
+fieldspec[42].name sourcefeedid
+fieldspec[42].searchmethod INT32
+fieldspec[42].arg1 ""
+fieldspec[43].name URI
+fieldspec[43].searchmethod AUTOUTF8
+fieldspec[43].arg1 ""
+fieldspec[44].name mailid
+fieldspec[44].searchmethod AUTOUTF8
+fieldspec[44].arg1 "prefix"
+fieldspec[45].name date
+fieldspec[45].searchmethod INT32
+fieldspec[45].arg1 ""
+fieldspec[46].name from
+fieldspec[46].searchmethod AUTOUTF8
+fieldspec[46].arg1 "prefix"
+fieldspec[47].name replyto
+fieldspec[47].searchmethod AUTOUTF8
+fieldspec[47].arg1 "prefix"
+fieldspec[48].name to
+fieldspec[48].searchmethod AUTOUTF8
+fieldspec[48].arg1 "prefix"
+fieldspec[49].name cc
+fieldspec[49].searchmethod AUTOUTF8
+fieldspec[49].arg1 "prefix"
+fieldspec[50].name bcc
+fieldspec[50].searchmethod AUTOUTF8
+fieldspec[50].arg1 "prefix"
+fieldspec[51].name subject
+fieldspec[51].searchmethod AUTOUTF8
+fieldspec[51].arg1 "prefix"
+fieldspec[52].name mailbody
+fieldspec[52].searchmethod AUTOUTF8
+fieldspec[52].arg1 "substring"
+fieldspec[53].name attachmentcount
+fieldspec[53].searchmethod INT32
+fieldspec[53].arg1 ""
+fieldspec[54].name attachmentnames
+fieldspec[54].searchmethod AUTOUTF8
+fieldspec[54].arg1 ""
+fieldspec[55].name attachmenttypes
+fieldspec[55].searchmethod AUTOUTF8
+fieldspec[55].arg1 ""
+fieldspec[56].name attachmentlanguages
+fieldspec[56].searchmethod AUTOUTF8
+fieldspec[56].arg1 "prefix"
+fieldspec[57].name attachmentcontent
+fieldspec[57].searchmethod AUTOUTF8
+fieldspec[57].arg1 "prefix"
+documenttype[7]
+documenttype[0].name newssummary
+documenttype[0].index[24]
+documenttype[0].index[0].name sddocname
+documenttype[0].index[0].field[1]
+documenttype[0].index[0].field[0].name sddocname
+documenttype[0].index[1].name title
+documenttype[0].index[1].field[1]
+documenttype[0].index[1].field[0].name title
+documenttype[0].index[2].name titleabstract
+documenttype[0].index[2].field[2]
+documenttype[0].index[2].field[0].name title
+documenttype[0].index[2].field[1].name abstract
+documenttype[0].index[3].name default
+documenttype[0].index[3].field[2]
+documenttype[0].index[3].field[0].name title
+documenttype[0].index[3].field[1].name abstract
+documenttype[0].index[4].name abstract
+documenttype[0].index[4].field[1]
+documenttype[0].index[4].field[0].name abstract
+documenttype[0].index[5].name sourcename
+documenttype[0].index[5].field[1]
+documenttype[0].index[5].field[0].name sourcename
+documenttype[0].index[6].name source
+documenttype[0].index[6].field[2]
+documenttype[0].index[6].field[0].name sourcename
+documenttype[0].index[6].field[1].name providername
+documenttype[0].index[7].name providername
+documenttype[0].index[7].field[1]
+documenttype[0].index[7].field[0].name providername
+documenttype[0].index[8].name thumburl
+documenttype[0].index[8].field[1]
+documenttype[0].index[8].field[0].name thumburl
+documenttype[0].index[9].name thumbwidth
+documenttype[0].index[9].field[1]
+documenttype[0].index[9].field[0].name thumbwidth
+documenttype[0].index[10].name thumbheight
+documenttype[0].index[10].field[1]
+documenttype[0].index[10].field[0].name thumbheight
+documenttype[0].index[11].name language
+documenttype[0].index[11].field[1]
+documenttype[0].index[11].field[0].name language
+documenttype[0].index[12].name languages
+documenttype[0].index[12].field[1]
+documenttype[0].index[12].field[0].name language
+documenttype[0].index[13].name url
+documenttype[0].index[13].field[1]
+documenttype[0].index[13].field[0].name url
+documenttype[0].index[14].name sourceurl
+documenttype[0].index[14].field[1]
+documenttype[0].index[14].field[0].name sourceurl
+documenttype[0].index[15].name categories
+documenttype[0].index[15].field[1]
+documenttype[0].index[15].field[0].name categories
+documenttype[0].index[16].name pubdate
+documenttype[0].index[16].field[1]
+documenttype[0].index[16].field[0].name pubdate
+documenttype[0].index[17].name expdate
+documenttype[0].index[17].field[1]
+documenttype[0].index[17].field[0].name expdate
+documenttype[0].index[18].name fingerprint
+documenttype[0].index[18].field[1]
+documenttype[0].index[18].field[0].name fingerprint
+documenttype[0].index[19].name debug
+documenttype[0].index[19].field[1]
+documenttype[0].index[19].field[0].name debug
+documenttype[0].index[20].name attributes
+documenttype[0].index[20].field[1]
+documenttype[0].index[20].field[0].name attributes
+documenttype[0].index[21].name eustaticrank
+documenttype[0].index[21].field[1]
+documenttype[0].index[21].field[0].name eustaticrank
+documenttype[0].index[22].name usstaticrank
+documenttype[0].index[22].field[1]
+documenttype[0].index[22].field[0].name usstaticrank
+documenttype[0].index[23].name asiastaticrank
+documenttype[0].index[23].field[1]
+documenttype[0].index[23].field[0].name asiastaticrank
+documenttype[1].name newsarticle
+documenttype[1].index[41]
+documenttype[1].index[0].name sddocname
+documenttype[1].index[0].field[1]
+documenttype[1].index[0].field[0].name sddocname
+documenttype[1].index[1].name title
+documenttype[1].index[1].field[1]
+documenttype[1].index[1].field[0].name title
+documenttype[1].index[2].name titleabstract
+documenttype[1].index[2].field[2]
+documenttype[1].index[2].field[0].name title
+documenttype[1].index[2].field[1].name abstract
+documenttype[1].index[3].name default
+documenttype[1].index[3].field[3]
+documenttype[1].index[3].field[0].name title
+documenttype[1].index[3].field[1].name abstract
+documenttype[1].index[3].field[2].name body
+documenttype[1].index[4].name abstract
+documenttype[1].index[4].field[1]
+documenttype[1].index[4].field[0].name abstract
+documenttype[1].index[5].name sourcename
+documenttype[1].index[5].field[1]
+documenttype[1].index[5].field[0].name sourcename
+documenttype[1].index[6].name source
+documenttype[1].index[6].field[3]
+documenttype[1].index[6].field[0].name sourcename
+documenttype[1].index[6].field[1].name providername
+documenttype[1].index[6].field[2].name othersourcenames
+documenttype[1].index[7].name providername
+documenttype[1].index[7].field[1]
+documenttype[1].index[7].field[0].name providername
+documenttype[1].index[8].name thumburl
+documenttype[1].index[8].field[1]
+documenttype[1].index[8].field[0].name thumburl
+documenttype[1].index[9].name thumbwidth
+documenttype[1].index[9].field[1]
+documenttype[1].index[9].field[0].name thumbwidth
+documenttype[1].index[10].name thumbheight
+documenttype[1].index[10].field[1]
+documenttype[1].index[10].field[0].name thumbheight
+documenttype[1].index[11].name language
+documenttype[1].index[11].field[1]
+documenttype[1].index[11].field[0].name language
+documenttype[1].index[12].name languages
+documenttype[1].index[12].field[2]
+documenttype[1].index[12].field[0].name language
+documenttype[1].index[12].field[1].name otherlanguages
+documenttype[1].index[13].name url
+documenttype[1].index[13].field[1]
+documenttype[1].index[13].field[0].name url
+documenttype[1].index[14].name sourceurl
+documenttype[1].index[14].field[1]
+documenttype[1].index[14].field[0].name sourceurl
+documenttype[1].index[15].name categories
+documenttype[1].index[15].field[1]
+documenttype[1].index[15].field[0].name categories
+documenttype[1].index[16].name pubdate
+documenttype[1].index[16].field[1]
+documenttype[1].index[16].field[0].name pubdate
+documenttype[1].index[17].name expdate
+documenttype[1].index[17].field[1]
+documenttype[1].index[17].field[0].name expdate
+documenttype[1].index[18].name fingerprint
+documenttype[1].index[18].field[1]
+documenttype[1].index[18].field[0].name fingerprint
+documenttype[1].index[19].name debug
+documenttype[1].index[19].field[1]
+documenttype[1].index[19].field[0].name debug
+documenttype[1].index[20].name attributes
+documenttype[1].index[20].field[1]
+documenttype[1].index[20].field[0].name attributes
+documenttype[1].index[21].name eustaticrank
+documenttype[1].index[21].field[1]
+documenttype[1].index[21].field[0].name eustaticrank
+documenttype[1].index[22].name usstaticrank
+documenttype[1].index[22].field[1]
+documenttype[1].index[22].field[0].name usstaticrank
+documenttype[1].index[23].name asiastaticrank
+documenttype[1].index[23].field[1]
+documenttype[1].index[23].field[0].name asiastaticrank
+documenttype[1].index[24].name body
+documenttype[1].index[24].field[1]
+documenttype[1].index[24].field[0].name body
+documenttype[1].index[25].name othersourcenames
+documenttype[1].index[25].field[1]
+documenttype[1].index[25].field[0].name othersourcenames
+documenttype[1].index[26].name author
+documenttype[1].index[26].field[1]
+documenttype[1].index[26].field[0].name author
+documenttype[1].index[27].name referrerurl
+documenttype[1].index[27].field[1]
+documenttype[1].index[27].field[0].name referrerurl
+documenttype[1].index[28].name sourcelocation
+documenttype[1].index[28].field[4]
+documenttype[1].index[28].field[0].name sourcelocation
+documenttype[1].index[28].field[1].name sourcecountry
+documenttype[1].index[28].field[2].name sourcelocale
+documenttype[1].index[28].field[3].name sourcecontinent
+documenttype[1].index[29].name sourcecountry
+documenttype[1].index[29].field[1]
+documenttype[1].index[29].field[0].name sourcecountry
+documenttype[1].index[30].name sourcelocale
+documenttype[1].index[30].field[1]
+documenttype[1].index[30].field[0].name sourcelocale
+documenttype[1].index[31].name sourcecontinent
+documenttype[1].index[31].field[1]
+documenttype[1].index[31].field[0].name sourcecontinent
+documenttype[1].index[32].name articlecountry
+documenttype[1].index[32].field[1]
+documenttype[1].index[32].field[0].name articlecountry
+documenttype[1].index[33].name articlelocale
+documenttype[1].index[33].field[1]
+documenttype[1].index[33].field[0].name articlelocale
+documenttype[1].index[34].name articlecontinent
+documenttype[1].index[34].field[1]
+documenttype[1].index[34].field[0].name articlecontinent
+documenttype[1].index[35].name sourcerank
+documenttype[1].index[35].field[1]
+documenttype[1].index[35].field[0].name sourcerank
+documenttype[1].index[36].name crawldate
+documenttype[1].index[36].field[1]
+documenttype[1].index[36].field[0].name crawldate
+documenttype[1].index[37].name indexdate
+documenttype[1].index[37].field[1]
+documenttype[1].index[37].field[0].name indexdate
+documenttype[1].index[38].name procdate
+documenttype[1].index[38].field[1]
+documenttype[1].index[38].field[0].name procdate
+documenttype[1].index[39].name sourceid
+documenttype[1].index[39].field[1]
+documenttype[1].index[39].field[0].name sourceid
+documenttype[1].index[40].name sourcefeedid
+documenttype[1].index[40].field[1]
+documenttype[1].index[40].field[0].name sourcefeedid
+documenttype[2].name music
+documenttype[2].index[1]
+documenttype[2].index[0].name sddocname
+documenttype[2].index[0].field[1]
+documenttype[2].index[0].field[0].name sddocname
+documenttype[3].name mail
+documenttype[3].index[21]
+documenttype[3].index[0].name sddocname
+documenttype[3].index[0].field[1]
+documenttype[3].index[0].field[0].name sddocname
+documenttype[3].index[1].name mailid
+documenttype[3].index[1].field[1]
+documenttype[3].index[1].field[0].name mailid
+documenttype[3].index[2].name date
+documenttype[3].index[2].field[1]
+documenttype[3].index[2].field[0].name date
+documenttype[3].index[3].name from
+documenttype[3].index[3].field[1]
+documenttype[3].index[3].field[0].name from
+documenttype[3].index[4].name sender
+documenttype[3].index[4].field[1]
+documenttype[3].index[4].field[0].name from
+documenttype[3].index[5].name address
+documenttype[3].index[5].field[3]
+documenttype[3].index[5].field[0].name from
+documenttype[3].index[5].field[1].name to
+documenttype[3].index[5].field[2].name cc
+documenttype[3].index[6].name header
+documenttype[3].index[6].field[4]
+documenttype[3].index[6].field[0].name from
+documenttype[3].index[6].field[1].name to
+documenttype[3].index[6].field[2].name cc
+documenttype[3].index[6].field[3].name subject
+documenttype[3].index[7].name default
+documenttype[3].index[7].field[5]
+documenttype[3].index[7].field[0].name from
+documenttype[3].index[7].field[1].name to
+documenttype[3].index[7].field[2].name cc
+documenttype[3].index[7].field[3].name subject
+documenttype[3].index[7].field[4].name mailbody
+documenttype[3].index[8].name all
+documenttype[3].index[8].field[8]
+documenttype[3].index[8].field[0].name from
+documenttype[3].index[8].field[1].name to
+documenttype[3].index[8].field[2].name cc
+documenttype[3].index[8].field[3].name subject
+documenttype[3].index[8].field[4].name mailbody
+documenttype[3].index[8].field[5].name attachmentnames
+documenttype[3].index[8].field[6].name attachmenttypes
+documenttype[3].index[8].field[7].name attachmentcontent
+documenttype[3].index[9].name replyto
+documenttype[3].index[9].field[1]
+documenttype[3].index[9].field[0].name replyto
+documenttype[3].index[10].name to
+documenttype[3].index[10].field[1]
+documenttype[3].index[10].field[0].name to
+documenttype[3].index[11].name recipient
+documenttype[3].index[11].field[2]
+documenttype[3].index[11].field[0].name to
+documenttype[3].index[11].field[1].name cc
+documenttype[3].index[12].name cc
+documenttype[3].index[12].field[1]
+documenttype[3].index[12].field[0].name cc
+documenttype[3].index[13].name bcc
+documenttype[3].index[13].field[1]
+documenttype[3].index[13].field[0].name bcc
+documenttype[3].index[14].name subject
+documenttype[3].index[14].field[1]
+documenttype[3].index[14].field[0].name subject
+documenttype[3].index[15].name mailbody
+documenttype[3].index[15].field[1]
+documenttype[3].index[15].field[0].name mailbody
+documenttype[3].index[16].name attachmentcount
+documenttype[3].index[16].field[1]
+documenttype[3].index[16].field[0].name attachmentcount
+documenttype[3].index[17].name attachmentname
+documenttype[3].index[17].field[1]
+documenttype[3].index[17].field[0].name attachmentnames
+documenttype[3].index[18].name attachmenttype
+documenttype[3].index[18].field[1]
+documenttype[3].index[18].field[0].name attachmenttypes
+documenttype[3].index[19].name attachmentlanguages
+documenttype[3].index[19].field[1]
+documenttype[3].index[19].field[0].name attachmentlanguages
+documenttype[3].index[20].name attachment
+documenttype[3].index[20].field[1]
+documenttype[3].index[20].field[0].name attachmentcontent
+documenttype[4].name compressed_header
+documenttype[4].index[1]
+documenttype[4].index[0].name sddocname
+documenttype[4].index[0].field[1]
+documenttype[4].index[0].field[0].name sddocname
+documenttype[5].name compressed_both
+documenttype[5].index[1]
+documenttype[5].index[0].name sddocname
+documenttype[5].index[0].field[1]
+documenttype[5].index[0].field[0].name sddocname
+documenttype[6].name compressed_body
+documenttype[6].index[1]
+documenttype[6].index[0].name sddocname
+documenttype[6].index[0].field[1]
+documenttype[6].index[0].field[0].name sddocname
diff --git a/config-model/src/test/derived/documentderiver/vsmsummary.cfg b/config-model/src/test/derived/documentderiver/vsmsummary.cfg
new file mode 100644
index 00000000000..5f9882e8c1b
--- /dev/null
+++ b/config-model/src/test/derived/documentderiver/vsmsummary.cfg
@@ -0,0 +1,6 @@
+fieldmap[1]
+fieldmap[0].summary snippet
+fieldmap[0].document[2]
+fieldmap[0].document[0].field body
+fieldmap[0].document[1].field attachmentcontent
+fieldmap[0].command FLATTENJUNIPER \ No newline at end of file
diff --git a/config-model/src/test/derived/emptychild/child.sd b/config-model/src/test/derived/emptychild/child.sd
new file mode 100644
index 00000000000..2e2423b91d6
--- /dev/null
+++ b/config-model/src/test/derived/emptychild/child.sd
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+ document child inherits parent {
+ }
+}
diff --git a/config-model/src/test/derived/emptychild/parent.sd b/config-model/src/test/derived/emptychild/parent.sd
new file mode 100644
index 00000000000..69d48de8f75
--- /dev/null
+++ b/config-model/src/test/derived/emptychild/parent.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search parent {
+ document parent {
+ field a1 type string {
+ indexing: attribute | summary
+ }
+ }
+}
diff --git a/config-model/src/test/derived/emptychild/summary.cfg b/config-model/src/test/derived/emptychild/summary.cfg
new file mode 100644
index 00000000000..b8b56eea8f5
--- /dev/null
+++ b/config-model/src/test/derived/emptychild/summary.cfg
@@ -0,0 +1,19 @@
+defaultsummaryid 1814603381
+classes[0].id 1814603381
+classes[0].name "default"
+classes[0].fields[0].name "a1"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "rankfeatures"
+classes[0].fields[1].type "featuredata"
+classes[0].fields[2].name "summaryfeatures"
+classes[0].fields[2].type "featuredata"
+classes[0].fields[3].name "documentid"
+classes[0].fields[3].type "longstring"
+classes[1].id 1490368133
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "a1"
+classes[1].fields[0].type "longstring"
+classes[1].fields[1].name "rankfeatures"
+classes[1].fields[1].type "featuredata"
+classes[1].fields[2].name "summaryfeatures"
+classes[1].fields[2].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/emptydefault/attributes.cfg b/config-model/src/test/derived/emptydefault/attributes.cfg
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/attributes.cfg
diff --git a/config-model/src/test/derived/emptydefault/documentmanager.cfg b/config-model/src/test/derived/emptydefault/documentmanager.cfg
new file mode 100644
index 00000000000..4235199342a
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/documentmanager.cfg
@@ -0,0 +1,43 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 461724009
+datatype[1].structtype[0].name "emptydefault.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "one"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[1].structtype[0].field[1].name "two"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "rankfeatures"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[1].structtype[0].field[3].name "summaryfeatures"
+datatype[1].structtype[0].field[3].datatype 2
+datatype[2].id 311791038
+datatype[2].structtype[0].name "emptydefault.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[3].id -1663995626
+datatype[3].documenttype[0].name "emptydefault"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct 461724009
+datatype[3].documenttype[0].bodystruct 311791038
+datatype[3].documenttype[0].fieldsets{[document]}.fields[0] "one"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[1] "two"
diff --git a/config-model/src/test/derived/emptydefault/emptydefault.sd b/config-model/src/test/derived/emptydefault/emptydefault.sd
new file mode 100644
index 00000000000..14c83003079
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/emptydefault.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search emptydefault {
+
+ document emptydefault {
+
+ field one type string {
+ indexing: index
+ }
+
+ field two type string {
+ indexing: index
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/emptydefault/ilscripts.cfg b/config-model/src/test/derived/emptydefault/ilscripts.cfg
new file mode 100644
index 00000000000..b28b5ade17f
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/ilscripts.cfg
@@ -0,0 +1,6 @@
+maxtermoccurrences 100
+ilscript[0].doctype "emptydefault"
+ilscript[0].docfield[0] "one"
+ilscript[0].docfield[1] "two"
+ilscript[0].content[0] "clear_state | guard { input one | tokenize normalize stem:\"SHORTEST\" | index one; }"
+ilscript[0].content[1] "clear_state | guard { input two | tokenize normalize stem:\"SHORTEST\" | index two; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/emptydefault/index-info.cfg b/config-model/src/test/derived/emptydefault/index-info.cfg
new file mode 100644
index 00000000000..0ec9439e13b
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/index-info.cfg
@@ -0,0 +1,25 @@
+indexinfo[0].name "emptydefault"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "one"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "one"
+indexinfo[0].command[3].command "lowercase"
+indexinfo[0].command[4].indexname "one"
+indexinfo[0].command[4].command "stem:SHORTEST"
+indexinfo[0].command[5].indexname "one"
+indexinfo[0].command[5].command "normalize"
+indexinfo[0].command[6].indexname "two"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "two"
+indexinfo[0].command[7].command "lowercase"
+indexinfo[0].command[8].indexname "two"
+indexinfo[0].command[8].command "stem:SHORTEST"
+indexinfo[0].command[9].indexname "two"
+indexinfo[0].command[9].command "normalize"
+indexinfo[0].command[10].indexname "rankfeatures"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "summaryfeatures"
+indexinfo[0].command[11].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/emptydefault/rank-profiles.cfg b/config-model/src/test/derived/emptydefault/rank-profiles.cfg
new file mode 100644
index 00000000000..caca83a9a91
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/rank-profiles.cfg
@@ -0,0 +1,10 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/emptydefault/summary.cfg b/config-model/src/test/derived/emptydefault/summary.cfg
new file mode 100644
index 00000000000..788f02babfc
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/summary.cfg
@@ -0,0 +1,9 @@
+defaultsummaryid 1151071433
+classes[0].id 1151071433
+classes[0].name "default"
+classes[0].fields[0].name "rankfeatures"
+classes[0].fields[0].type "featuredata"
+classes[0].fields[1].name "summaryfeatures"
+classes[0].fields[1].type "featuredata"
+classes[0].fields[2].name "documentid"
+classes[0].fields[2].type "longstring" \ No newline at end of file
diff --git a/config-model/src/test/derived/emptydefault/summarymap.cfg b/config-model/src/test/derived/emptydefault/summarymap.cfg
new file mode 100644
index 00000000000..42b6e811ee6
--- /dev/null
+++ b/config-model/src/test/derived/emptydefault/summarymap.cfg
@@ -0,0 +1,7 @@
+defaultoutputclass -1
+override[0].field "rankfeatures"
+override[0].command "rankfeatures"
+override[0].arguments ""
+override[1].field "summaryfeatures"
+override[1].command "summaryfeatures"
+override[1].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/exactmatch/exactmatch.sd b/config-model/src/test/derived/exactmatch/exactmatch.sd
new file mode 100644
index 00000000000..9eaea3654a7
--- /dev/null
+++ b/config-model/src/test/derived/exactmatch/exactmatch.sd
@@ -0,0 +1,21 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search exactmatch {
+
+ document exactmatch {
+
+ field tag type string {
+ indexing: summary | index
+ match: exact
+ }
+
+ field screweduserids type string {
+ indexing: index | summary | attribute
+ match {
+ exact
+ exact-terminator: "*!!!*"
+ }
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/exactmatch/ilscripts.cfg b/config-model/src/test/derived/exactmatch/ilscripts.cfg
new file mode 100644
index 00000000000..c3ecab6d3a9
--- /dev/null
+++ b/config-model/src/test/derived/exactmatch/ilscripts.cfg
@@ -0,0 +1,6 @@
+maxtermoccurrences 100
+ilscript[0].doctype "exactmatch"
+ilscript[0].docfield[0] "tag"
+ilscript[0].docfield[1] "screweduserids"
+ilscript[0].content[0] "clear_state | guard { input tag | exact | summary tag | index tag; }"
+ilscript[0].content[1] "clear_state | guard { input screweduserids | exact | index screweduserids | summary screweduserids | attribute screweduserids; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/exactmatch/index-info.cfg b/config-model/src/test/derived/exactmatch/index-info.cfg
new file mode 100644
index 00000000000..aeece43832d
--- /dev/null
+++ b/config-model/src/test/derived/exactmatch/index-info.cfg
@@ -0,0 +1,21 @@
+indexinfo[0].name "exactmatch"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "tag"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "tag"
+indexinfo[0].command[3].command "lowercase"
+indexinfo[0].command[4].indexname "tag"
+indexinfo[0].command[4].command "exact @@"
+indexinfo[0].command[5].indexname "screweduserids"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "screweduserids"
+indexinfo[0].command[6].command "lowercase"
+indexinfo[0].command[7].indexname "screweduserids"
+indexinfo[0].command[7].command "exact *!!!*"
+indexinfo[0].command[8].indexname "rankfeatures"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "summaryfeatures"
+indexinfo[0].command[9].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/fieldlength/attributes.cfg b/config-model/src/test/derived/fieldlength/attributes.cfg
new file mode 100644
index 00000000000..29fd2f986f0
--- /dev/null
+++ b/config-model/src/test/derived/fieldlength/attributes.cfg
@@ -0,0 +1,4 @@
+attribute[1]
+attribute[0].name "year"
+attribute[0].datatype INT32
+attribute[0].collectiontype SINGLE
diff --git a/config-model/src/test/derived/fieldlength/fieldlength.sd b/config-model/src/test/derived/fieldlength/fieldlength.sd
new file mode 100644
index 00000000000..df42b822dc2
--- /dev/null
+++ b/config-model/src/test/derived/fieldlength/fieldlength.sd
@@ -0,0 +1,73 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search fieldlength {
+
+ document fieldlength {
+
+ field artist type string {
+ indexing: summary | index
+ # index-to: default
+ }
+
+ field title type string {
+ indexing: summary | index
+ # index-to: default
+ }
+
+ field song type string {
+ indexing: summary | index
+ # index-to: all, song
+ }
+
+ field album type string {
+ indexing: summary | index
+ # index-to: all, album, all2
+ }
+
+ field composer type string {
+ indexing: summary | index
+ # index-to: all, composer, all2
+ }
+
+ field label type string {
+ indexing: summary | index
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ }
+
+ }
+
+ rank-profile default {
+ first-phase {
+ expression: classicRank
+ }
+ second-phase {
+ expression: if(3>2,4,2)
+ rerank-count: 10
+ }
+ rank-features: attribute(baz).out sum(value(3))
+ rank-features: classicRank
+ ignore-default-rank-features
+
+ rank-properties {
+ foo: "bar, baz"
+ qux: "quux"
+ foo: "foobar"
+ }
+
+ }
+
+ rank-profile static {
+ first-phase {
+ expression: attribute
+ }
+ second-phase {
+ expression: file:../rankexpression/rankexpression
+ }
+ summary-features: sum(value(1),value(2))
+ }
+
+}
+
+
diff --git a/config-model/src/test/derived/flickr/flickrphotos.sd b/config-model/src/test/derived/flickr/flickrphotos.sd
new file mode 100755
index 00000000000..8a2232ad2b0
--- /dev/null
+++ b/config-model/src/test/derived/flickr/flickrphotos.sd
@@ -0,0 +1,24 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search flickrphotos{
+
+ #Document summary to use for attribute-prefetching with many hits
+
+ document-summary mapcluster {
+ summary distance type int {}
+ }
+
+ document flickrphotos{
+
+
+ field loc type string{
+ indexing: summary | to_pos | attribute
+ }
+
+ }
+
+
+
+
+
+}#end flickrphotos cluster
+
diff --git a/config-model/src/test/derived/gemini2/gemini.sd b/config-model/src/test/derived/gemini2/gemini.sd
new file mode 100644
index 00000000000..cfa85d0e2a9
--- /dev/null
+++ b/config-model/src/test/derived/gemini2/gemini.sd
@@ -0,0 +1,27 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search gemini {
+
+ document gemini {
+ }
+
+ rank-profile test {
+
+ macro wrapper2(x) {
+ expression: x
+ }
+
+ macro wrapper1(x) {
+ expression: wrapper2(x)
+ }
+
+ macro toplevel() {
+ expression: wrapper1(attribute(right))
+ }
+
+ macro interfering() {
+ expression: wrapper1(attribute(wrong))
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/gemini2/rank-profiles.cfg b/config-model/src/test/derived/gemini2/rank-profiles.cfg
new file mode 100644
index 00000000000..362155fe6cc
--- /dev/null
+++ b/config-model/src/test/derived/gemini2/rank-profiles.cfg
@@ -0,0 +1,29 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[2].name "test"
+rankprofile[2].fef.property[0].name "rankingExpression(wrapper2).rankingScript"
+rankprofile[2].fef.property[0].value "x"
+rankprofile[2].fef.property[1].name "rankingExpression(wrapper2@c7ef616469cdb0b3).rankingScript"
+rankprofile[2].fef.property[1].value "x"
+rankprofile[2].fef.property[2].name "rankingExpression(wrapper1).rankingScript"
+rankprofile[2].fef.property[2].value "rankingExpression(wrapper2@c7ef616469cdb0b3)"
+rankprofile[2].fef.property[3].name "rankingExpression(wrapper2@2d437c13405e61d6).rankingScript"
+rankprofile[2].fef.property[3].value "attribute(right)"
+rankprofile[2].fef.property[4].name "rankingExpression(wrapper1@2d437c13405e61d6).rankingScript"
+rankprofile[2].fef.property[4].value "rankingExpression(wrapper2@2d437c13405e61d6)"
+rankprofile[2].fef.property[5].name "rankingExpression(toplevel).rankingScript"
+rankprofile[2].fef.property[5].value "rankingExpression(wrapper1@2d437c13405e61d6)"
+rankprofile[2].fef.property[6].name "rankingExpression(wrapper2@8fc8470e911f253f).rankingScript"
+rankprofile[2].fef.property[6].value "attribute(wrong)"
+rankprofile[2].fef.property[7].name "rankingExpression(wrapper1@8fc8470e911f253f).rankingScript"
+rankprofile[2].fef.property[7].value "rankingExpression(wrapper2@8fc8470e911f253f)"
+rankprofile[2].fef.property[8].name "rankingExpression(interfering).rankingScript"
+rankprofile[2].fef.property[8].value "rankingExpression(wrapper1@8fc8470e911f253f)" \ No newline at end of file
diff --git a/config-model/src/test/derived/id/attributes.cfg b/config-model/src/test/derived/id/attributes.cfg
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/derived/id/attributes.cfg
diff --git a/config-model/src/test/derived/id/documentmanager.cfg b/config-model/src/test/derived/id/documentmanager.cfg
new file mode 100644
index 00000000000..40996d46399
--- /dev/null
+++ b/config-model/src/test/derived/id/documentmanager.cfg
@@ -0,0 +1,40 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -531633022
+datatype[1].structtype[0].name "id.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "uri"
+datatype[1].structtype[0].field[0].datatype 10
+datatype[1].structtype[0].field[1].name "rankfeatures"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "summaryfeatures"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[2].id -1830022377
+datatype[2].structtype[0].name "id.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[3].id 3225629
+datatype[3].documenttype[0].name "id"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct -531633022
+datatype[3].documenttype[0].bodystruct -1830022377
+datatype[3].documenttype[0].fieldsets{[document]}.fields[0] "uri"
diff --git a/config-model/src/test/derived/id/id.sd b/config-model/src/test/derived/id/id.sd
new file mode 100644
index 00000000000..aac5463775c
--- /dev/null
+++ b/config-model/src/test/derived/id/id.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search id {
+
+ document id {
+
+ field uri type uri {
+ indexing: summary | index
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/id/ilscripts.cfg b/config-model/src/test/derived/id/ilscripts.cfg
new file mode 100644
index 00000000000..c613431cdf9
--- /dev/null
+++ b/config-model/src/test/derived/id/ilscripts.cfg
@@ -0,0 +1,4 @@
+maxtermoccurrences 100
+ilscript[0].doctype "id"
+ilscript[0].docfield[0] "uri"
+ilscript[0].content[0] "clear_state | guard { input uri | summary uri | index uri; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/id/index-info.cfg b/config-model/src/test/derived/id/index-info.cfg
new file mode 100644
index 00000000000..e54e8640d13
--- /dev/null
+++ b/config-model/src/test/derived/id/index-info.cfg
@@ -0,0 +1,45 @@
+indexinfo[0].name "id"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "uri"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "uri"
+indexinfo[0].command[3].command "lowercase"
+indexinfo[0].command[4].indexname "uri"
+indexinfo[0].command[4].command "fullurl"
+indexinfo[0].command[5].indexname "uri.uri"
+indexinfo[0].command[5].command "fullurl"
+indexinfo[0].command[6].indexname "uri.uri"
+indexinfo[0].command[6].command "lowercase"
+indexinfo[0].command[7].indexname "uri.path"
+indexinfo[0].command[7].command "fullurl"
+indexinfo[0].command[8].indexname "uri.path"
+indexinfo[0].command[8].command "lowercase"
+indexinfo[0].command[9].indexname "uri.query"
+indexinfo[0].command[9].command "fullurl"
+indexinfo[0].command[10].indexname "uri.query"
+indexinfo[0].command[10].command "lowercase"
+indexinfo[0].command[11].indexname "uri.hostname"
+indexinfo[0].command[11].command "urlhost"
+indexinfo[0].command[12].indexname "uri.hostname"
+indexinfo[0].command[12].command "lowercase"
+indexinfo[0].command[13].indexname "rankfeatures"
+indexinfo[0].command[13].command "index"
+indexinfo[0].command[14].indexname "summaryfeatures"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "uri.fragment"
+indexinfo[0].command[15].command "index"
+indexinfo[0].command[16].indexname "uri.host"
+indexinfo[0].command[16].command "index"
+indexinfo[0].command[17].indexname "uri.hostname"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "uri.path"
+indexinfo[0].command[18].command "index"
+indexinfo[0].command[19].indexname "uri.port"
+indexinfo[0].command[19].command "index"
+indexinfo[0].command[20].indexname "uri.query"
+indexinfo[0].command[20].command "index"
+indexinfo[0].command[21].indexname "uri.scheme"
+indexinfo[0].command[21].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/id/rank-profiles.cfg b/config-model/src/test/derived/id/rank-profiles.cfg
new file mode 100644
index 00000000000..caca83a9a91
--- /dev/null
+++ b/config-model/src/test/derived/id/rank-profiles.cfg
@@ -0,0 +1,10 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/id/summary.cfg b/config-model/src/test/derived/id/summary.cfg
new file mode 100644
index 00000000000..e67b81746b8
--- /dev/null
+++ b/config-model/src/test/derived/id/summary.cfg
@@ -0,0 +1,11 @@
+defaultsummaryid 1814716401
+classes[0].id 1814716401
+classes[0].name "default"
+classes[0].fields[0].name "uri"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "rankfeatures"
+classes[0].fields[1].type "featuredata"
+classes[0].fields[2].name "summaryfeatures"
+classes[0].fields[2].type "featuredata"
+classes[0].fields[3].name "documentid"
+classes[0].fields[3].type "longstring" \ No newline at end of file
diff --git a/config-model/src/test/derived/id/summarymap.cfg b/config-model/src/test/derived/id/summarymap.cfg
new file mode 100644
index 00000000000..42b6e811ee6
--- /dev/null
+++ b/config-model/src/test/derived/id/summarymap.cfg
@@ -0,0 +1,7 @@
+defaultoutputclass -1
+override[0].field "rankfeatures"
+override[0].command "rankfeatures"
+override[0].arguments ""
+override[1].field "summaryfeatures"
+override[1].command "summaryfeatures"
+override[1].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/id/vsmsummary.cfg b/config-model/src/test/derived/id/vsmsummary.cfg
new file mode 100644
index 00000000000..3538ba9880e
--- /dev/null
+++ b/config-model/src/test/derived/id/vsmsummary.cfg
@@ -0,0 +1,8 @@
+outputclass ""
+fieldmap[0].summary "uri"
+fieldmap[0].document[0].field "uri"
+fieldmap[0].command NONE
+fieldmap[1].summary "rankfeatures"
+fieldmap[1].command NONE
+fieldmap[2].summary "summaryfeatures"
+fieldmap[2].command NONE \ No newline at end of file
diff --git a/config-model/src/test/derived/indexinfo_fieldsets/index-info.cfg b/config-model/src/test/derived/indexinfo_fieldsets/index-info.cfg
new file mode 100644
index 00000000000..34dbb7d5e5b
--- /dev/null
+++ b/config-model/src/test/derived/indexinfo_fieldsets/index-info.cfg
@@ -0,0 +1,74 @@
+indexinfo[].name "indexinfo_fieldsets"
+indexinfo[].command[].indexname "sddocname"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "sddocname"
+indexinfo[].command[].command "word"
+
+indexinfo[].command[].indexname "nostemming1"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "nostemming1"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "nostemming1"
+indexinfo[].command[].command "normalize"
+
+indexinfo[].command[].indexname "nostemming2"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "nostemming2"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "nostemming2"
+indexinfo[].command[].command "normalize"
+
+indexinfo[].command[].indexname "nonormalizing1"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "nonormalizing1"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "nonormalizing1"
+indexinfo[].command[].command "stem:SHORTEST"
+
+indexinfo[].command[].indexname "nonormalizing2"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "nonormalizing2"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "nonormalizing2"
+indexinfo[].command[].command "stem:SHORTEST"
+
+indexinfo[].command[].indexname "exact1"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "exact1"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "exact1"
+indexinfo[].command[].command "exact @@"
+
+indexinfo[].command[].indexname "exact2"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "exact2"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "exact2"
+indexinfo[].command[].command "exact @@"
+
+indexinfo[].command[].indexname "rankfeatures"
+indexinfo[].command[].command "index"
+
+indexinfo[].command[].indexname "summaryfeatures"
+indexinfo[].command[].command "index"
+
+indexinfo[].command[].indexname "nostemming"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "nostemming"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "nostemming"
+indexinfo[].command[].command "normalize"
+
+indexinfo[].command[].indexname "nonormalizing"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "nonormalizing"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "nonormalizing"
+indexinfo[].command[].command "stem:SHORTEST"
+
+indexinfo[].command[].indexname "exact"
+indexinfo[].command[].command "lowercase"
+indexinfo[].command[].indexname "exact"
+indexinfo[].command[].command "index"
+indexinfo[].command[].indexname "exact"
+indexinfo[].command[].command "exact @@"
diff --git a/config-model/src/test/derived/indexinfo_fieldsets/indexinfo_fieldsets.sd b/config-model/src/test/derived/indexinfo_fieldsets/indexinfo_fieldsets.sd
new file mode 100644
index 00000000000..449a5d0a02e
--- /dev/null
+++ b/config-model/src/test/derived/indexinfo_fieldsets/indexinfo_fieldsets.sd
@@ -0,0 +1,50 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexinfo_fieldsets {
+
+ document indexinfo_fieldsets {
+
+ field nostemming1 type string {
+ indexing: summary | index
+ stemming: none
+ }
+
+ field nostemming2 type string {
+ indexing: summary | index
+ stemming: none
+ }
+
+ field nonormalizing1 type string {
+ indexing: summary | index
+ normalizing: none
+ }
+
+ field nonormalizing2 type string {
+ indexing: summary | index
+ normalizing: none
+ }
+
+ field exact1 type string {
+ indexing: summary | index
+ match: exact
+ }
+
+ field exact2 type string {
+ indexing: summary | index
+ match: exact
+ }
+
+ }
+
+ fieldset nostemming {
+ fields: nostemming1, nostemming2
+ }
+
+ fieldset nonormalizing {
+ fields: nonormalizing1, nonormalizing2
+ }
+
+ fieldset exact {
+ fields: exact1, exact2
+ }
+
+}
diff --git a/config-model/src/test/derived/indexinfo_lowercase/index-info.cfg b/config-model/src/test/derived/indexinfo_lowercase/index-info.cfg
new file mode 100644
index 00000000000..027e7533004
--- /dev/null
+++ b/config-model/src/test/derived/indexinfo_lowercase/index-info.cfg
@@ -0,0 +1,249 @@
+indexinfo[0].name "indexinfo_lowercase"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "lc_attribute_src"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "lc_index_src"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "lc_summary_src"
+indexinfo[0].command[4].command "index"
+indexinfo[0].command[5].indexname "nc_attribute"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "nc_attribute"
+indexinfo[0].command[6].command "attribute"
+indexinfo[0].command[7].indexname "nc_attribute"
+indexinfo[0].command[7].command "word"
+indexinfo[0].command[8].indexname "nc_index"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "nc_index"
+indexinfo[0].command[9].command "lowercase"
+indexinfo[0].command[10].indexname "nc_index"
+indexinfo[0].command[10].command "stem:SHORTEST"
+indexinfo[0].command[11].indexname "nc_index"
+indexinfo[0].command[11].command "normalize"
+indexinfo[0].command[12].indexname "nc_summary"
+indexinfo[0].command[12].command "index"
+indexinfo[0].command[13].indexname "lc_attribute"
+indexinfo[0].command[13].command "index"
+indexinfo[0].command[14].indexname "lc_attribute"
+indexinfo[0].command[14].command "lowercase"
+indexinfo[0].command[15].indexname "lc_attribute"
+indexinfo[0].command[15].command "attribute"
+indexinfo[0].command[16].indexname "lc_attribute"
+indexinfo[0].command[16].command "word"
+indexinfo[0].command[17].indexname "lc_index"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "lc_index"
+indexinfo[0].command[18].command "lowercase"
+indexinfo[0].command[19].indexname "lc_index"
+indexinfo[0].command[19].command "stem:SHORTEST"
+indexinfo[0].command[20].indexname "lc_index"
+indexinfo[0].command[20].command "normalize"
+indexinfo[0].command[21].indexname "lc_summary"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "lc_summary"
+indexinfo[0].command[22].command "lowercase"
+indexinfo[0].command[23].indexname "rankfeatures"
+indexinfo[0].command[23].command "index"
+indexinfo[0].command[24].indexname "summaryfeatures"
+indexinfo[0].command[24].command "index"
+indexinfo[0].command[25].indexname "lc_set1"
+indexinfo[0].command[25].command "lowercase"
+indexinfo[0].command[26].indexname "lc_set1"
+indexinfo[0].command[26].command "attribute"
+indexinfo[0].command[27].indexname "lc_set1"
+indexinfo[0].command[27].command "index"
+indexinfo[0].command[28].indexname "lc_set1"
+indexinfo[0].command[28].command "word"
+indexinfo[0].command[29].indexname "lc_set2"
+indexinfo[0].command[29].command "lowercase"
+indexinfo[0].command[30].indexname "lc_set2"
+indexinfo[0].command[30].command "index"
+indexinfo[0].command[31].indexname "lc_set2"
+indexinfo[0].command[31].command "stem:SHORTEST"
+indexinfo[0].command[32].indexname "lc_set2"
+indexinfo[0].command[32].command "normalize"
+indexinfo[0].command[33].indexname "lc_set3"
+indexinfo[0].command[33].command "lowercase"
+indexinfo[0].command[34].indexname "lc_set3"
+indexinfo[0].command[34].command "attribute"
+indexinfo[0].command[35].indexname "lc_set3"
+indexinfo[0].command[35].command "index"
+indexinfo[0].command[36].indexname "lc_set4"
+indexinfo[0].command[36].command "lowercase"
+indexinfo[0].command[37].indexname "lc_set4"
+indexinfo[0].command[37].command "index"
+indexinfo[0].command[38].indexname "lc_set4"
+indexinfo[0].command[38].command "stem:SHORTEST"
+indexinfo[0].command[39].indexname "lc_set4"
+indexinfo[0].command[39].command "normalize"
+indexinfo[0].command[40].indexname "lc_set5"
+indexinfo[0].command[40].command "lowercase"
+indexinfo[0].command[41].indexname "lc_set5"
+indexinfo[0].command[41].command "attribute"
+indexinfo[0].command[42].indexname "lc_set5"
+indexinfo[0].command[42].command "index"
+indexinfo[0].command[43].indexname "lc_set5"
+indexinfo[0].command[43].command "word"
+indexinfo[0].command[44].indexname "lc_set6"
+indexinfo[0].command[44].command "lowercase"
+indexinfo[0].command[45].indexname "lc_set6"
+indexinfo[0].command[45].command "index"
+indexinfo[0].command[46].indexname "lc_set6"
+indexinfo[0].command[46].command "stem:SHORTEST"
+indexinfo[0].command[47].indexname "lc_set6"
+indexinfo[0].command[47].command "normalize"
+indexinfo[0].command[48].indexname "lc_set7"
+indexinfo[0].command[48].command "lowercase"
+indexinfo[0].command[49].indexname "lc_set7"
+indexinfo[0].command[49].command "index"
+indexinfo[0].command[50].indexname "lc_set7"
+indexinfo[0].command[50].command "stem:SHORTEST"
+indexinfo[0].command[51].indexname "lc_set7"
+indexinfo[0].command[51].command "normalize"
+indexinfo[0].command[52].indexname "nc_set1"
+indexinfo[0].command[52].command "attribute"
+indexinfo[0].command[53].indexname "nc_set1"
+indexinfo[0].command[53].command "index"
+indexinfo[0].command[54].indexname "nc_set1"
+indexinfo[0].command[54].command "word"
+indexinfo[0].command[55].indexname "nc_set2"
+indexinfo[0].command[55].command "lowercase"
+indexinfo[0].command[56].indexname "nc_set2"
+indexinfo[0].command[56].command "index"
+indexinfo[0].command[57].indexname "nc_set2"
+indexinfo[0].command[57].command "stem:SHORTEST"
+indexinfo[0].command[58].indexname "nc_set2"
+indexinfo[0].command[58].command "normalize"
+indexinfo[0].command[59].indexname "nc_set3"
+indexinfo[0].command[59].command "attribute"
+indexinfo[0].command[60].indexname "nc_set3"
+indexinfo[0].command[60].command "index"
+indexinfo[0].command[61].indexname "nc_set4"
+indexinfo[0].command[61].command "lowercase"
+indexinfo[0].command[62].indexname "nc_set4"
+indexinfo[0].command[62].command "index"
+indexinfo[0].command[63].indexname "nc_set4"
+indexinfo[0].command[63].command "stem:SHORTEST"
+indexinfo[0].command[64].indexname "nc_set4"
+indexinfo[0].command[64].command "normalize"
+indexinfo[0].command[65].indexname "nc_set5"
+indexinfo[0].command[65].command "attribute"
+indexinfo[0].command[66].indexname "nc_set5"
+indexinfo[0].command[66].command "index"
+indexinfo[0].command[67].indexname "nc_set5"
+indexinfo[0].command[67].command "word"
+indexinfo[0].command[68].indexname "nc_set6"
+indexinfo[0].command[68].command "lowercase"
+indexinfo[0].command[69].indexname "nc_set6"
+indexinfo[0].command[69].command "index"
+indexinfo[0].command[70].indexname "nc_set6"
+indexinfo[0].command[70].command "stem:SHORTEST"
+indexinfo[0].command[71].indexname "nc_set6"
+indexinfo[0].command[71].command "normalize"
+indexinfo[0].command[72].indexname "nc_set7"
+indexinfo[0].command[72].command "lowercase"
+indexinfo[0].command[73].indexname "nc_set7"
+indexinfo[0].command[73].command "index"
+indexinfo[0].command[74].indexname "nc_set7"
+indexinfo[0].command[74].command "stem:SHORTEST"
+indexinfo[0].command[75].indexname "nc_set7"
+indexinfo[0].command[75].command "normalize"
+indexinfo[0].command[76].indexname "nc_set8"
+indexinfo[0].command[76].command "lowercase"
+indexinfo[0].command[77].indexname "nc_set8"
+indexinfo[0].command[77].command "index"
+indexinfo[0].command[78].indexname "nc_set8"
+indexinfo[0].command[78].command "stem:SHORTEST"
+indexinfo[0].command[79].indexname "nc_set8"
+indexinfo[0].command[79].command "normalize"
+indexinfo[0].command[80].indexname "nc_set9"
+indexinfo[0].command[80].command "lowercase"
+indexinfo[0].command[81].indexname "nc_set9"
+indexinfo[0].command[81].command "index"
+indexinfo[0].command[82].indexname "nc_set9"
+indexinfo[0].command[82].command "stem:SHORTEST"
+indexinfo[0].command[83].indexname "nc_set9"
+indexinfo[0].command[83].command "normalize"
+indexinfo[0].command[84].indexname "nc_set10"
+indexinfo[0].command[84].command "lowercase"
+indexinfo[0].command[85].indexname "nc_set10"
+indexinfo[0].command[85].command "attribute"
+indexinfo[0].command[86].indexname "nc_set10"
+indexinfo[0].command[86].command "index"
+indexinfo[0].command[87].indexname "nc_set10"
+indexinfo[0].command[87].command "word"
+indexinfo[0].command[88].indexname "nc_set11"
+indexinfo[0].command[88].command "lowercase"
+indexinfo[0].command[89].indexname "nc_set11"
+indexinfo[0].command[89].command "attribute"
+indexinfo[0].command[90].indexname "nc_set11"
+indexinfo[0].command[90].command "index"
+indexinfo[0].command[91].indexname "nc_set11"
+indexinfo[0].command[91].command "word"
+indexinfo[0].command[92].indexname "nc_set12"
+indexinfo[0].command[92].command "lowercase"
+indexinfo[0].command[93].indexname "nc_set12"
+indexinfo[0].command[93].command "index"
+indexinfo[0].command[94].indexname "nc_set12"
+indexinfo[0].command[94].command "stem:SHORTEST"
+indexinfo[0].command[95].indexname "nc_set12"
+indexinfo[0].command[95].command "normalize"
+indexinfo[0].command[96].indexname "nc_set13"
+indexinfo[0].command[96].command "lowercase"
+indexinfo[0].command[97].indexname "nc_set13"
+indexinfo[0].command[97].command "index"
+indexinfo[0].command[98].indexname "nc_set13"
+indexinfo[0].command[98].command "stem:SHORTEST"
+indexinfo[0].command[99].indexname "nc_set13"
+indexinfo[0].command[99].command "normalize"
+indexinfo[0].command[100].indexname "nc_set14"
+indexinfo[0].command[100].command "lowercase"
+indexinfo[0].command[101].indexname "nc_set14"
+indexinfo[0].command[101].command "index"
+indexinfo[0].command[102].indexname "nc_set14"
+indexinfo[0].command[102].command "stem:SHORTEST"
+indexinfo[0].command[103].indexname "nc_set14"
+indexinfo[0].command[103].command "normalize"
+indexinfo[0].command[104].indexname "nc_set15"
+indexinfo[0].command[104].command "lowercase"
+indexinfo[0].command[105].indexname "nc_set15"
+indexinfo[0].command[105].command "index"
+indexinfo[0].command[106].indexname "nc_set15"
+indexinfo[0].command[106].command "stem:SHORTEST"
+indexinfo[0].command[107].indexname "nc_set15"
+indexinfo[0].command[107].command "normalize"
+indexinfo[0].command[108].indexname "nc_set16"
+indexinfo[0].command[108].command "lowercase"
+indexinfo[0].command[109].indexname "nc_set16"
+indexinfo[0].command[109].command "index"
+indexinfo[0].command[110].indexname "nc_set16"
+indexinfo[0].command[110].command "stem:SHORTEST"
+indexinfo[0].command[111].indexname "nc_set16"
+indexinfo[0].command[111].command "normalize"
+indexinfo[0].command[112].indexname "nc_set17"
+indexinfo[0].command[112].command "lowercase"
+indexinfo[0].command[113].indexname "nc_set17"
+indexinfo[0].command[113].command "index"
+indexinfo[0].command[114].indexname "nc_set17"
+indexinfo[0].command[114].command "stem:SHORTEST"
+indexinfo[0].command[115].indexname "nc_set17"
+indexinfo[0].command[115].command "normalize"
+indexinfo[0].command[116].indexname "nc_set18"
+indexinfo[0].command[116].command "lowercase"
+indexinfo[0].command[117].indexname "nc_set18"
+indexinfo[0].command[117].command "index"
+indexinfo[0].command[118].indexname "nc_set18"
+indexinfo[0].command[118].command "stem:SHORTEST"
+indexinfo[0].command[119].indexname "nc_set18"
+indexinfo[0].command[119].command "normalize"
+indexinfo[0].command[120].indexname "nc_set19"
+indexinfo[0].command[120].command "lowercase"
+indexinfo[0].command[121].indexname "nc_set19"
+indexinfo[0].command[121].command "index"
+indexinfo[0].command[122].indexname "nc_set19"
+indexinfo[0].command[122].command "stem:SHORTEST"
+indexinfo[0].command[123].indexname "nc_set19"
+indexinfo[0].command[123].command "normalize" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexinfo_lowercase/indexinfo_lowercase.sd b/config-model/src/test/derived/indexinfo_lowercase/indexinfo_lowercase.sd
new file mode 100644
index 00000000000..951a36d0817
--- /dev/null
+++ b/config-model/src/test/derived/indexinfo_lowercase/indexinfo_lowercase.sd
@@ -0,0 +1,120 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexinfo_lowercase {
+
+ document indexinfo_lowercase {
+ field lc_attribute_src type string {
+
+ }
+ field lc_index_src type string {
+
+ }
+ field lc_summary_src type string {
+
+ }
+
+ # these fields do not lower case any input, and should not lower case query
+ field nc_attribute type string {
+ indexing: attribute
+ }
+ field nc_index type string {
+ indexing: index
+ }
+ field nc_summary type string {
+ indexing: summary
+ }
+ }
+
+ # these fields lower case all input, and should lower case query
+ field lc_attribute type string {
+ indexing: input lc_attribute_src | lowercase | attribute
+ }
+ field lc_index type string {
+ indexing: input lc_index_src | lowercase | index
+ }
+ field lc_summary type string {
+ indexing: input lc_summary_src | lowercase | summary
+ }
+
+ # these field sets contain only lower cased fields, and should lower case query
+ fieldset lc_set1 {
+ fields: lc_attribute
+ }
+ fieldset lc_set2 {
+ fields: lc_index
+ }
+ fieldset lc_set3 {
+ fields: lc_summary
+ }
+ fieldset lc_set4 {
+ fields: lc_attribute, lc_index
+ }
+ fieldset lc_set5 {
+ fields: lc_attribute, lc_summary
+ }
+ fieldset lc_set6 {
+ fields: lc_index, lc_summary
+ }
+ fieldset lc_set7 {
+ fields: lc_attribute, lc_index, lc_summary
+ }
+
+ # these field sets contain at least one non-lower cased field, and should not lower case query
+ fieldset nc_set1 {
+ fields: nc_attribute
+ }
+ fieldset nc_set2 {
+ fields: nc_index
+ }
+ fieldset nc_set3 {
+ fields: nc_summary
+ }
+ fieldset nc_set4 {
+ fields: nc_attribute, nc_index
+ }
+ fieldset nc_set5 {
+ fields: nc_attribute, nc_summary
+ }
+ fieldset nc_set6 {
+ fields: nc_index, nc_summary
+ }
+ fieldset nc_set7 {
+ fields: nc_attribute, nc_index, nc_summary
+ }
+ fieldset nc_set8 {
+ fields: lc_attribute, nc_index
+ }
+ fieldset nc_set9 {
+ fields: nc_attribute, lc_index
+ }
+ fieldset nc_set10 {
+ fields: lc_attribute, nc_summary
+ }
+ fieldset nc_set11 {
+ fields: nc_attribute, lc_summary
+ }
+ fieldset nc_set12 {
+ fields: lc_index, nc_summary
+ }
+ fieldset nc_set13 {
+ fields: nc_index, lc_summary
+ }
+ fieldset nc_set14 {
+ fields: lc_attribute, nc_index, nc_summary
+ }
+ fieldset nc_set15 {
+ fields: lc_attribute, lc_index, nc_summary
+ }
+ fieldset nc_set16 {
+ fields: lc_attribute, nc_index, lc_summary
+ }
+ fieldset nc_set17 {
+ fields: nc_attribute, lc_index, nc_summary
+ }
+ fieldset nc_set18 {
+ fields: nc_attribute, lc_index, lc_summary
+ }
+ fieldset nc_set19 {
+ fields: nc_attribute, nc_index, lc_summary
+ }
+
+}
diff --git a/config-model/src/test/derived/indexschema/index-info.cfg b/config-model/src/test/derived/indexschema/index-info.cfg
new file mode 100644
index 00000000000..ca5818b9b88
--- /dev/null
+++ b/config-model/src/test/derived/indexschema/index-info.cfg
@@ -0,0 +1,311 @@
+indexinfo[0].name "indexschema"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "sa"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "sa"
+indexinfo[0].command[3].command "lowercase"
+indexinfo[0].command[4].indexname "sa"
+indexinfo[0].command[4].command "stem:SHORTEST"
+indexinfo[0].command[5].indexname "sa"
+indexinfo[0].command[5].command "normalize"
+indexinfo[0].command[6].indexname "sb"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "sb"
+indexinfo[0].command[7].command "lowercase"
+indexinfo[0].command[8].indexname "sb"
+indexinfo[0].command[8].command "stem:SHORTEST"
+indexinfo[0].command[9].indexname "sb"
+indexinfo[0].command[9].command "normalize"
+indexinfo[0].command[10].indexname "sc"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "sc"
+indexinfo[0].command[11].command "lowercase"
+indexinfo[0].command[12].indexname "sc"
+indexinfo[0].command[12].command "stem:SHORTEST"
+indexinfo[0].command[13].indexname "sc"
+indexinfo[0].command[13].command "normalize"
+indexinfo[0].command[14].indexname "sd"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "sd"
+indexinfo[0].command[15].command "lowercase"
+indexinfo[0].command[16].indexname "sd"
+indexinfo[0].command[16].command "stem:SHORTEST"
+indexinfo[0].command[17].indexname "sd"
+indexinfo[0].command[17].command "normalize"
+indexinfo[0].command[18].indexname "sd"
+indexinfo[0].command[18].command "literal-boost"
+indexinfo[0].command[19].indexname "pos.x"
+indexinfo[0].command[19].command "index"
+indexinfo[0].command[20].indexname "pos.x"
+indexinfo[0].command[20].command "numerical"
+indexinfo[0].command[21].indexname "pos.y"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "pos.y"
+indexinfo[0].command[22].command "numerical"
+indexinfo[0].command[23].indexname "pos"
+indexinfo[0].command[23].command "default-position"
+indexinfo[0].command[24].indexname "pos"
+indexinfo[0].command[24].command "index"
+indexinfo[0].command[25].indexname "se"
+indexinfo[0].command[25].command "index"
+indexinfo[0].command[26].indexname "se"
+indexinfo[0].command[26].command "attribute"
+indexinfo[0].command[27].indexname "se"
+indexinfo[0].command[27].command "word"
+indexinfo[0].command[28].indexname "sf"
+indexinfo[0].command[28].command "index"
+indexinfo[0].command[29].indexname "sf"
+indexinfo[0].command[29].command "lowercase"
+indexinfo[0].command[30].indexname "sf"
+indexinfo[0].command[30].command "multivalue"
+indexinfo[0].command[31].indexname "sf"
+indexinfo[0].command[31].command "stem:SHORTEST"
+indexinfo[0].command[32].indexname "sf"
+indexinfo[0].command[32].command "normalize"
+indexinfo[0].command[33].indexname "sg"
+indexinfo[0].command[33].command "index"
+indexinfo[0].command[34].indexname "sg"
+indexinfo[0].command[34].command "lowercase"
+indexinfo[0].command[35].indexname "sg"
+indexinfo[0].command[35].command "multivalue"
+indexinfo[0].command[36].indexname "sg"
+indexinfo[0].command[36].command "stem:SHORTEST"
+indexinfo[0].command[37].indexname "sg"
+indexinfo[0].command[37].command "normalize"
+indexinfo[0].command[38].indexname "sh"
+indexinfo[0].command[38].command "index"
+indexinfo[0].command[39].indexname "sh"
+indexinfo[0].command[39].command "lowercase"
+indexinfo[0].command[40].indexname "sh"
+indexinfo[0].command[40].command "fullurl"
+indexinfo[0].command[41].indexname "sh.sh"
+indexinfo[0].command[41].command "fullurl"
+indexinfo[0].command[42].indexname "sh.sh"
+indexinfo[0].command[42].command "lowercase"
+indexinfo[0].command[43].indexname "sh.path"
+indexinfo[0].command[43].command "fullurl"
+indexinfo[0].command[44].indexname "sh.path"
+indexinfo[0].command[44].command "lowercase"
+indexinfo[0].command[45].indexname "sh.query"
+indexinfo[0].command[45].command "fullurl"
+indexinfo[0].command[46].indexname "sh.query"
+indexinfo[0].command[46].command "lowercase"
+indexinfo[0].command[47].indexname "sh.hostname"
+indexinfo[0].command[47].command "urlhost"
+indexinfo[0].command[48].indexname "sh.hostname"
+indexinfo[0].command[48].command "lowercase"
+indexinfo[0].command[49].indexname "si"
+indexinfo[0].command[49].command "index"
+indexinfo[0].command[50].indexname "si"
+indexinfo[0].command[50].command "lowercase"
+indexinfo[0].command[51].indexname "si"
+indexinfo[0].command[51].command "stem:SHORTEST"
+indexinfo[0].command[52].indexname "si"
+indexinfo[0].command[52].command "normalize"
+indexinfo[0].command[53].indexname "exact1"
+indexinfo[0].command[53].command "index"
+indexinfo[0].command[54].indexname "exact1"
+indexinfo[0].command[54].command "lowercase"
+indexinfo[0].command[55].indexname "exact1"
+indexinfo[0].command[55].command "exact @@"
+indexinfo[0].command[56].indexname "exact2"
+indexinfo[0].command[56].command "index"
+indexinfo[0].command[57].indexname "exact2"
+indexinfo[0].command[57].command "lowercase"
+indexinfo[0].command[58].indexname "exact2"
+indexinfo[0].command[58].command "exact @@"
+indexinfo[0].command[59].indexname "ia"
+indexinfo[0].command[59].command "index"
+indexinfo[0].command[60].indexname "ia"
+indexinfo[0].command[60].command "attribute"
+indexinfo[0].command[61].indexname "ia"
+indexinfo[0].command[61].command "numerical"
+indexinfo[0].command[62].indexname "ib"
+indexinfo[0].command[62].command "index"
+indexinfo[0].command[63].indexname "ib"
+indexinfo[0].command[63].command "attribute"
+indexinfo[0].command[64].indexname "ib"
+indexinfo[0].command[64].command "numerical"
+indexinfo[0].command[65].indexname "ic"
+indexinfo[0].command[65].command "index"
+indexinfo[0].command[66].indexname "ic"
+indexinfo[0].command[66].command "attribute"
+indexinfo[0].command[67].indexname "ic"
+indexinfo[0].command[67].command "numerical"
+indexinfo[0].command[68].indexname "nostemstring1"
+indexinfo[0].command[68].command "index"
+indexinfo[0].command[69].indexname "nostemstring1"
+indexinfo[0].command[69].command "lowercase"
+indexinfo[0].command[70].indexname "nostemstring1"
+indexinfo[0].command[70].command "normalize"
+indexinfo[0].command[71].indexname "nostemstring2"
+indexinfo[0].command[71].command "index"
+indexinfo[0].command[72].indexname "nostemstring2"
+indexinfo[0].command[72].command "lowercase"
+indexinfo[0].command[73].indexname "nostemstring2"
+indexinfo[0].command[73].command "normalize"
+indexinfo[0].command[74].indexname "nostemstring3"
+indexinfo[0].command[74].command "index"
+indexinfo[0].command[75].indexname "nostemstring3"
+indexinfo[0].command[75].command "lowercase"
+indexinfo[0].command[76].indexname "nostemstring3"
+indexinfo[0].command[76].command "normalize"
+indexinfo[0].command[77].indexname "nostemstring4"
+indexinfo[0].command[77].command "index"
+indexinfo[0].command[78].indexname "nostemstring4"
+indexinfo[0].command[78].command "lowercase"
+indexinfo[0].command[79].indexname "nostemstring4"
+indexinfo[0].command[79].command "normalize"
+indexinfo[0].command[80].indexname "fs9"
+indexinfo[0].command[80].command "index"
+indexinfo[0].command[81].indexname "fs9"
+indexinfo[0].command[81].command "lowercase"
+indexinfo[0].command[82].indexname "fs9"
+indexinfo[0].command[82].command "stem:SHORTEST"
+indexinfo[0].command[83].indexname "fs9"
+indexinfo[0].command[83].command "normalize"
+indexinfo[0].command[84].indexname "f10.text"
+indexinfo[0].command[84].command "index"
+indexinfo[0].command[85].indexname "f10.text"
+indexinfo[0].command[85].command "lowercase"
+indexinfo[0].command[86].indexname "f10.text"
+indexinfo[0].command[86].command "stem:SHORTEST"
+indexinfo[0].command[87].indexname "f10.text"
+indexinfo[0].command[87].command "normalize"
+indexinfo[0].command[88].indexname "f10.name"
+indexinfo[0].command[88].command "index"
+indexinfo[0].command[89].indexname "f10"
+indexinfo[0].command[89].command "index"
+indexinfo[0].command[90].indexname "f10"
+indexinfo[0].command[90].command "multivalue"
+indexinfo[0].command[91].indexname "pos.distance"
+indexinfo[0].command[91].command "index"
+indexinfo[0].command[92].indexname "pos.distance"
+indexinfo[0].command[92].command "numerical"
+indexinfo[0].command[93].indexname "pos.position"
+indexinfo[0].command[93].command "index"
+indexinfo[0].command[94].indexname "pos.position"
+indexinfo[0].command[94].command "multivalue"
+indexinfo[0].command[95].indexname "pos_zcurve"
+indexinfo[0].command[95].command "index"
+indexinfo[0].command[96].indexname "pos_zcurve"
+indexinfo[0].command[96].command "attribute"
+indexinfo[0].command[97].indexname "pos_zcurve"
+indexinfo[0].command[97].command "fast-search"
+indexinfo[0].command[98].indexname "pos_zcurve"
+indexinfo[0].command[98].command "numerical"
+indexinfo[0].command[99].indexname "rankfeatures"
+indexinfo[0].command[99].command "index"
+indexinfo[0].command[100].indexname "sd_literal"
+indexinfo[0].command[100].command "index"
+indexinfo[0].command[101].indexname "sd_literal"
+indexinfo[0].command[101].command "lowercase"
+indexinfo[0].command[102].indexname "searchfield1"
+indexinfo[0].command[102].command "index"
+indexinfo[0].command[103].indexname "searchfield2"
+indexinfo[0].command[103].command "index"
+indexinfo[0].command[104].indexname "searchfield2"
+indexinfo[0].command[104].command "numerical"
+indexinfo[0].command[105].indexname "sh.fragment"
+indexinfo[0].command[105].command "index"
+indexinfo[0].command[106].indexname "sh.host"
+indexinfo[0].command[106].command "index"
+indexinfo[0].command[107].indexname "sh.hostname"
+indexinfo[0].command[107].command "index"
+indexinfo[0].command[108].indexname "sh.path"
+indexinfo[0].command[108].command "index"
+indexinfo[0].command[109].indexname "sh.port"
+indexinfo[0].command[109].command "index"
+indexinfo[0].command[110].indexname "sh.query"
+indexinfo[0].command[110].command "index"
+indexinfo[0].command[111].indexname "sh.scheme"
+indexinfo[0].command[111].command "index"
+indexinfo[0].command[112].indexname "summaryfeatures"
+indexinfo[0].command[112].command "index"
+indexinfo[0].command[113].indexname "sa"
+indexinfo[0].command[113].command "dynteaser"
+indexinfo[0].command[114].indexname "fs1"
+indexinfo[0].command[114].command "lowercase"
+indexinfo[0].command[115].indexname "fs1"
+indexinfo[0].command[115].command "multivalue"
+indexinfo[0].command[116].indexname "fs1"
+indexinfo[0].command[116].command "index"
+indexinfo[0].command[117].indexname "fs1"
+indexinfo[0].command[117].command "stem:SHORTEST"
+indexinfo[0].command[118].indexname "fs1"
+indexinfo[0].command[118].command "normalize"
+indexinfo[0].command[119].indexname "fs2"
+indexinfo[0].command[119].command "lowercase"
+indexinfo[0].command[120].indexname "fs2"
+indexinfo[0].command[120].command "index"
+indexinfo[0].command[121].indexname "fs2"
+indexinfo[0].command[121].command "stem:SHORTEST"
+indexinfo[0].command[122].indexname "fs2"
+indexinfo[0].command[122].command "normalize"
+indexinfo[0].command[123].indexname "fs3"
+indexinfo[0].command[123].command "attribute"
+indexinfo[0].command[124].indexname "fs3"
+indexinfo[0].command[124].command "index"
+indexinfo[0].command[125].indexname "fs4"
+indexinfo[0].command[125].command "lowercase"
+indexinfo[0].command[126].indexname "fs4"
+indexinfo[0].command[126].command "index"
+indexinfo[0].command[127].indexname "fs4"
+indexinfo[0].command[127].command "stem:SHORTEST"
+indexinfo[0].command[128].indexname "fs4"
+indexinfo[0].command[128].command "normalize"
+indexinfo[0].command[129].indexname "onlyattrib"
+indexinfo[0].command[129].command "attribute"
+indexinfo[0].command[130].indexname "onlyattrib"
+indexinfo[0].command[130].command "index"
+indexinfo[0].command[131].indexname "onlyattrib"
+indexinfo[0].command[131].command "word"
+indexinfo[0].command[132].indexname "exactfields"
+indexinfo[0].command[132].command "lowercase"
+indexinfo[0].command[133].indexname "exactfields"
+indexinfo[0].command[133].command "index"
+indexinfo[0].command[134].indexname "exactfields"
+indexinfo[0].command[134].command "exact @@"
+indexinfo[0].command[135].indexname "exactexplicit"
+indexinfo[0].command[135].command "exact ARNOLD"
+indexinfo[0].command[136].indexname "exactexplicit"
+indexinfo[0].command[136].command "dynteaser"
+indexinfo[0].command[137].indexname "exactexplicit2"
+indexinfo[0].command[137].command "lowercase"
+indexinfo[0].command[138].indexname "exactexplicit2"
+indexinfo[0].command[138].command "index"
+indexinfo[0].command[139].indexname "exactexplicit2"
+indexinfo[0].command[139].command "exact Arnold"
+indexinfo[0].command[140].indexname "gram"
+indexinfo[0].command[140].command "lowercase"
+indexinfo[0].command[141].indexname "gram"
+indexinfo[0].command[141].command "index"
+indexinfo[0].command[142].indexname "gram"
+indexinfo[0].command[142].command "stem:SHORTEST"
+indexinfo[0].command[143].indexname "gram"
+indexinfo[0].command[143].command "normalize"
+indexinfo[0].command[144].indexname "gram"
+indexinfo[0].command[144].command "ngram 2"
+indexinfo[0].command[145].indexname "nostem1"
+indexinfo[0].command[145].command "lowercase"
+indexinfo[0].command[146].indexname "nostem1"
+indexinfo[0].command[146].command "index"
+indexinfo[0].command[147].indexname "nostem1"
+indexinfo[0].command[147].command "normalize"
+indexinfo[0].command[148].indexname "nostem2"
+indexinfo[0].command[148].command "lowercase"
+indexinfo[0].command[149].indexname "nostem2"
+indexinfo[0].command[149].command "index"
+indexinfo[0].command[150].indexname "nostem2"
+indexinfo[0].command[150].command "normalize"
+indexinfo[0].command[151].indexname "default"
+indexinfo[0].command[151].command "lowercase"
+indexinfo[0].command[152].indexname "default"
+indexinfo[0].command[152].command "index"
+indexinfo[0].command[153].indexname "default"
+indexinfo[0].command[153].command "stem:SHORTEST"
+indexinfo[0].command[154].indexname "default"
+indexinfo[0].command[154].command "normalize" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexschema/indexschema.cfg b/config-model/src/test/derived/indexschema/indexschema.cfg
new file mode 100644
index 00000000000..459d2ff37b4
--- /dev/null
+++ b/config-model/src/test/derived/indexschema/indexschema.cfg
@@ -0,0 +1,221 @@
+indexfield[0].name "sa"
+indexfield[0].indextype VESPA
+indexfield[0].datatype STRING
+indexfield[0].collectiontype SINGLE
+indexfield[0].prefix false
+indexfield[0].phrases false
+indexfield[0].positions true
+indexfield[0].averageelementlen 512
+indexfield[1].name "sb"
+indexfield[1].indextype VESPA
+indexfield[1].datatype STRING
+indexfield[1].collectiontype SINGLE
+indexfield[1].prefix false
+indexfield[1].phrases false
+indexfield[1].positions true
+indexfield[1].averageelementlen 512
+indexfield[2].name "sc"
+indexfield[2].indextype VESPA
+indexfield[2].datatype STRING
+indexfield[2].collectiontype SINGLE
+indexfield[2].prefix false
+indexfield[2].phrases false
+indexfield[2].positions true
+indexfield[2].averageelementlen 512
+indexfield[3].name "sd"
+indexfield[3].indextype VESPA
+indexfield[3].datatype STRING
+indexfield[3].collectiontype SINGLE
+indexfield[3].prefix false
+indexfield[3].phrases false
+indexfield[3].positions true
+indexfield[3].averageelementlen 512
+indexfield[4].name "sf"
+indexfield[4].indextype VESPA
+indexfield[4].datatype STRING
+indexfield[4].collectiontype ARRAY
+indexfield[4].prefix false
+indexfield[4].phrases false
+indexfield[4].positions true
+indexfield[4].averageelementlen 512
+indexfield[5].name "sg"
+indexfield[5].indextype VESPA
+indexfield[5].datatype STRING
+indexfield[5].collectiontype WEIGHTEDSET
+indexfield[5].prefix false
+indexfield[5].phrases false
+indexfield[5].positions true
+indexfield[5].averageelementlen 512
+indexfield[6].name "sh"
+indexfield[6].indextype VESPA
+indexfield[6].datatype STRING
+indexfield[6].collectiontype SINGLE
+indexfield[6].prefix false
+indexfield[6].phrases false
+indexfield[6].positions true
+indexfield[6].averageelementlen 512
+indexfield[7].name "si"
+indexfield[7].indextype VESPA
+indexfield[7].datatype STRING
+indexfield[7].collectiontype SINGLE
+indexfield[7].prefix true
+indexfield[7].phrases false
+indexfield[7].positions true
+indexfield[7].averageelementlen 512
+indexfield[8].name "exact1"
+indexfield[8].indextype VESPA
+indexfield[8].datatype STRING
+indexfield[8].collectiontype SINGLE
+indexfield[8].prefix false
+indexfield[8].phrases false
+indexfield[8].positions true
+indexfield[8].averageelementlen 512
+indexfield[9].name "exact2"
+indexfield[9].indextype VESPA
+indexfield[9].datatype STRING
+indexfield[9].collectiontype SINGLE
+indexfield[9].prefix false
+indexfield[9].phrases false
+indexfield[9].positions true
+indexfield[9].averageelementlen 512
+indexfield[10].name "nostemstring1"
+indexfield[10].indextype VESPA
+indexfield[10].datatype STRING
+indexfield[10].collectiontype SINGLE
+indexfield[10].prefix false
+indexfield[10].phrases false
+indexfield[10].positions true
+indexfield[10].averageelementlen 512
+indexfield[11].name "nostemstring2"
+indexfield[11].indextype VESPA
+indexfield[11].datatype STRING
+indexfield[11].collectiontype SINGLE
+indexfield[11].prefix false
+indexfield[11].phrases false
+indexfield[11].positions true
+indexfield[11].averageelementlen 512
+indexfield[12].name "nostemstring3"
+indexfield[12].indextype VESPA
+indexfield[12].datatype STRING
+indexfield[12].collectiontype SINGLE
+indexfield[12].prefix false
+indexfield[12].phrases false
+indexfield[12].positions true
+indexfield[12].averageelementlen 512
+indexfield[13].name "nostemstring4"
+indexfield[13].indextype VESPA
+indexfield[13].datatype STRING
+indexfield[13].collectiontype SINGLE
+indexfield[13].prefix false
+indexfield[13].phrases false
+indexfield[13].positions true
+indexfield[13].averageelementlen 512
+indexfield[14].name "fs9"
+indexfield[14].indextype VESPA
+indexfield[14].datatype STRING
+indexfield[14].collectiontype SINGLE
+indexfield[14].prefix false
+indexfield[14].phrases false
+indexfield[14].positions true
+indexfield[14].averageelementlen 512
+indexfield[15].name "sd_literal"
+indexfield[15].indextype VESPA
+indexfield[15].datatype STRING
+indexfield[15].collectiontype SINGLE
+indexfield[15].prefix false
+indexfield[15].phrases false
+indexfield[15].positions true
+indexfield[15].averageelementlen 512
+indexfield[16].name "sh.fragment"
+indexfield[16].indextype VESPA
+indexfield[16].datatype STRING
+indexfield[16].collectiontype SINGLE
+indexfield[16].prefix false
+indexfield[16].phrases false
+indexfield[16].positions true
+indexfield[16].averageelementlen 512
+indexfield[17].name "sh.host"
+indexfield[17].indextype VESPA
+indexfield[17].datatype STRING
+indexfield[17].collectiontype SINGLE
+indexfield[17].prefix false
+indexfield[17].phrases false
+indexfield[17].positions true
+indexfield[17].averageelementlen 512
+indexfield[18].name "sh.hostname"
+indexfield[18].indextype VESPA
+indexfield[18].datatype STRING
+indexfield[18].collectiontype SINGLE
+indexfield[18].prefix false
+indexfield[18].phrases false
+indexfield[18].positions true
+indexfield[18].averageelementlen 512
+indexfield[19].name "sh.path"
+indexfield[19].indextype VESPA
+indexfield[19].datatype STRING
+indexfield[19].collectiontype SINGLE
+indexfield[19].prefix false
+indexfield[19].phrases false
+indexfield[19].positions true
+indexfield[19].averageelementlen 512
+indexfield[20].name "sh.port"
+indexfield[20].indextype VESPA
+indexfield[20].datatype STRING
+indexfield[20].collectiontype SINGLE
+indexfield[20].prefix false
+indexfield[20].phrases false
+indexfield[20].positions true
+indexfield[20].averageelementlen 512
+indexfield[21].name "sh.query"
+indexfield[21].indextype VESPA
+indexfield[21].datatype STRING
+indexfield[21].collectiontype SINGLE
+indexfield[21].prefix false
+indexfield[21].phrases false
+indexfield[21].positions true
+indexfield[21].averageelementlen 512
+indexfield[22].name "sh.scheme"
+indexfield[22].indextype VESPA
+indexfield[22].datatype STRING
+indexfield[22].collectiontype SINGLE
+indexfield[22].prefix false
+indexfield[22].phrases false
+indexfield[22].positions true
+indexfield[22].averageelementlen 512
+fieldset[0].name "fs9"
+fieldset[0].field[0].name "se"
+fieldset[1].name "fs1"
+fieldset[1].field[0].name "se"
+fieldset[1].field[1].name "sf"
+fieldset[1].field[2].name "sg"
+fieldset[2].name "fs2"
+fieldset[2].field[0].name "si"
+fieldset[3].name "fs3"
+fieldset[3].field[0].name "ib"
+fieldset[3].field[1].name "ic"
+fieldset[4].name "fs4"
+fieldset[4].field[0].name "sa"
+fieldset[4].field[1].name "sb"
+fieldset[5].name "onlyattrib"
+fieldset[5].field[0].name "se"
+fieldset[6].name "exactfields"
+fieldset[6].field[0].name "exact1"
+fieldset[6].field[1].name "exact2"
+fieldset[7].name "exactexplicit"
+fieldset[7].field[0].name "sa"
+fieldset[7].field[1].name "sb"
+fieldset[8].name "exactexplicit2"
+fieldset[8].field[0].name "sc"
+fieldset[8].field[1].name "sd"
+fieldset[9].name "gram"
+fieldset[9].field[0].name "sc"
+fieldset[9].field[1].name "sd"
+fieldset[10].name "nostem1"
+fieldset[10].field[0].name "nostemstring1"
+fieldset[10].field[1].name "nostemstring2"
+fieldset[11].name "nostem2"
+fieldset[11].field[0].name "nostemstring3"
+fieldset[11].field[1].name "nostemstring4"
+fieldset[12].name "default"
+fieldset[12].field[0].name "sb"
+fieldset[12].field[1].name "sc" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexschema/indexschema.sd b/config-model/src/test/derived/indexschema/indexschema.sd
new file mode 100644
index 00000000000..60c732c7c94
--- /dev/null
+++ b/config-model/src/test/derived/indexschema/indexschema.sd
@@ -0,0 +1,161 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexschema {
+
+ field searchfield1 type string {}
+ field searchfield2 type int {}
+
+ document indexschema {
+ struct part {
+ field text type string {}
+ field name type string {}
+ }
+
+ # string fields
+ field sa type string {
+ indexing: index
+ summary: dynamic
+ }
+ field sb type string {
+ indexing: index
+ }
+ field sc type string {
+ indexing: index
+ }
+ field sd type string {
+ indexing: index
+ rank:literal
+ }
+ field pos type position {
+ indexing: attribute
+ }
+ field se type string {
+ indexing: attribute
+ }
+ field sf type array<string> {
+ indexing: index
+ }
+ field sg type weightedset<string> {
+ indexing: index
+ }
+ field sh type uri {
+ indexing: index
+ }
+ field si type string {
+ indexing: index
+ index {
+ prefix
+ }
+ }
+ field exact1 type string {
+ indexing: index
+ match: exact
+ }
+ field exact2 type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ # integer fields
+ field ia type int {
+ indexing: index
+ }
+ field ib type int {
+ indexing: index
+ }
+ field ic type int {
+ indexing: attribute
+ }
+
+ field nostemstring1 type string {
+ indexing: index | summary
+ index {
+ stemming: none
+ }
+ }
+
+ field nostemstring2 type string {
+ indexing: index | summary
+ index {
+ stemming: none
+ }
+ }
+
+ field nostemstring3 type string {
+ indexing: index | summary
+ stemming: none
+ }
+
+ field nostemstring4 type string {
+ indexing: index | summary
+ stemming: none
+ }
+
+ field fs9 type string {
+ indexing:index|summary
+ }
+
+ field f10 type array<part> {
+ struct-field text {
+ indexing: index|summary
+ }
+ }
+
+ }
+ fieldset fs9 {
+ fields:se
+ }
+ fieldset fs1 {
+ fields: se, sf, sg
+ }
+ fieldset fs2 {
+ fields: si
+ }
+ fieldset fs3 {
+ fields: ib, ic
+ }
+ fieldset fs4 {
+ fields: sa, sb
+ }
+ fieldset onlyattrib {
+ fields: se
+ }
+ fieldset exactfields {
+ fields: exact1, exact2
+ }
+
+ fieldset exactexplicit {
+ fields:sa, sb
+ query-command: "exact ARNOLD"
+ query-command: dynteaser
+ }
+
+ fieldset exactexplicit2 {
+ fields:sc, sd
+ match {
+ exact
+ exact-terminator: "Arnold"
+ }
+ }
+
+ fieldset gram {
+ fields: sc, sd
+ match: gram
+ }
+
+ fieldset nostem1 {
+ fields: nostemstring1, nostemstring2
+ }
+
+ fieldset nostem2 {
+ fields: nostemstring3, nostemstring4
+ }
+
+ fieldset default {
+ fields: sb, sc
+ }
+
+
+}
+
diff --git a/config-model/src/test/derived/indexschema/vsmfields.cfg b/config-model/src/test/derived/indexschema/vsmfields.cfg
new file mode 100644
index 00000000000..74d41e1cb6f
--- /dev/null
+++ b/config-model/src/test/derived/indexschema/vsmfields.cfg
@@ -0,0 +1,194 @@
+documentverificationlevel 0
+searchall 1
+fieldspec[0].name "sa"
+fieldspec[0].searchmethod AUTOUTF8
+fieldspec[0].arg1 ""
+fieldspec[0].maxlength 1048576
+fieldspec[0].fieldtype INDEX
+fieldspec[1].name "sb"
+fieldspec[1].searchmethod AUTOUTF8
+fieldspec[1].arg1 ""
+fieldspec[1].maxlength 1048576
+fieldspec[1].fieldtype INDEX
+fieldspec[2].name "sc"
+fieldspec[2].searchmethod AUTOUTF8
+fieldspec[2].arg1 ""
+fieldspec[2].maxlength 1048576
+fieldspec[2].fieldtype INDEX
+fieldspec[3].name "sd"
+fieldspec[3].searchmethod AUTOUTF8
+fieldspec[3].arg1 ""
+fieldspec[3].maxlength 1048576
+fieldspec[3].fieldtype INDEX
+fieldspec[4].name "se"
+fieldspec[4].searchmethod AUTOUTF8
+fieldspec[4].arg1 "word"
+fieldspec[4].maxlength 1048576
+fieldspec[4].fieldtype ATTRIBUTE
+fieldspec[5].name "sf"
+fieldspec[5].searchmethod AUTOUTF8
+fieldspec[5].arg1 ""
+fieldspec[5].maxlength 1048576
+fieldspec[5].fieldtype INDEX
+fieldspec[6].name "sg"
+fieldspec[6].searchmethod AUTOUTF8
+fieldspec[6].arg1 ""
+fieldspec[6].maxlength 1048576
+fieldspec[6].fieldtype INDEX
+fieldspec[7].name "sh"
+fieldspec[7].searchmethod AUTOUTF8
+fieldspec[7].arg1 ""
+fieldspec[7].maxlength 1048576
+fieldspec[7].fieldtype INDEX
+fieldspec[8].name "si"
+fieldspec[8].searchmethod AUTOUTF8
+fieldspec[8].arg1 ""
+fieldspec[8].maxlength 1048576
+fieldspec[8].fieldtype INDEX
+fieldspec[9].name "exact1"
+fieldspec[9].searchmethod AUTOUTF8
+fieldspec[9].arg1 "exact"
+fieldspec[9].maxlength 1048576
+fieldspec[9].fieldtype INDEX
+fieldspec[10].name "exact2"
+fieldspec[10].searchmethod AUTOUTF8
+fieldspec[10].arg1 "exact"
+fieldspec[10].maxlength 1048576
+fieldspec[10].fieldtype INDEX
+fieldspec[11].name "ia"
+fieldspec[11].searchmethod INT32
+fieldspec[11].arg1 ""
+fieldspec[11].maxlength 1048576
+fieldspec[11].fieldtype ATTRIBUTE
+fieldspec[12].name "ib"
+fieldspec[12].searchmethod INT32
+fieldspec[12].arg1 ""
+fieldspec[12].maxlength 1048576
+fieldspec[12].fieldtype ATTRIBUTE
+fieldspec[13].name "ic"
+fieldspec[13].searchmethod INT32
+fieldspec[13].arg1 ""
+fieldspec[13].maxlength 1048576
+fieldspec[13].fieldtype ATTRIBUTE
+fieldspec[14].name "nostemstring1"
+fieldspec[14].searchmethod AUTOUTF8
+fieldspec[14].arg1 ""
+fieldspec[14].maxlength 1048576
+fieldspec[14].fieldtype INDEX
+fieldspec[15].name "nostemstring2"
+fieldspec[15].searchmethod AUTOUTF8
+fieldspec[15].arg1 ""
+fieldspec[15].maxlength 1048576
+fieldspec[15].fieldtype INDEX
+fieldspec[16].name "nostemstring3"
+fieldspec[16].searchmethod AUTOUTF8
+fieldspec[16].arg1 ""
+fieldspec[16].maxlength 1048576
+fieldspec[16].fieldtype INDEX
+fieldspec[17].name "nostemstring4"
+fieldspec[17].searchmethod AUTOUTF8
+fieldspec[17].arg1 ""
+fieldspec[17].maxlength 1048576
+fieldspec[17].fieldtype INDEX
+fieldspec[18].name "fs9"
+fieldspec[18].searchmethod AUTOUTF8
+fieldspec[18].arg1 ""
+fieldspec[18].maxlength 1048576
+fieldspec[18].fieldtype INDEX
+fieldspec[19].name "f10.text"
+fieldspec[19].searchmethod AUTOUTF8
+fieldspec[19].arg1 ""
+fieldspec[19].maxlength 1048576
+fieldspec[19].fieldtype INDEX
+fieldspec[20].name "sd_literal"
+fieldspec[20].searchmethod AUTOUTF8
+fieldspec[20].arg1 ""
+fieldspec[20].maxlength 1048576
+fieldspec[20].fieldtype INDEX
+fieldspec[21].name "pos_zcurve"
+fieldspec[21].searchmethod INT64
+fieldspec[21].arg1 ""
+fieldspec[21].maxlength 1048576
+fieldspec[21].fieldtype ATTRIBUTE
+documenttype[0].name "indexschema"
+documenttype[0].index[0].name "sa"
+documenttype[0].index[0].field[0].name "sa"
+documenttype[0].index[1].name "sb"
+documenttype[0].index[1].field[0].name "sb"
+documenttype[0].index[2].name "sc"
+documenttype[0].index[2].field[0].name "sc"
+documenttype[0].index[3].name "sd"
+documenttype[0].index[3].field[0].name "sd"
+documenttype[0].index[4].name "se"
+documenttype[0].index[4].field[0].name "se"
+documenttype[0].index[5].name "sf"
+documenttype[0].index[5].field[0].name "sf"
+documenttype[0].index[6].name "sg"
+documenttype[0].index[6].field[0].name "sg"
+documenttype[0].index[7].name "sh"
+documenttype[0].index[7].field[0].name "sh"
+documenttype[0].index[8].name "si"
+documenttype[0].index[8].field[0].name "si"
+documenttype[0].index[9].name "exact1"
+documenttype[0].index[9].field[0].name "exact1"
+documenttype[0].index[10].name "exact2"
+documenttype[0].index[10].field[0].name "exact2"
+documenttype[0].index[11].name "ia"
+documenttype[0].index[11].field[0].name "ia"
+documenttype[0].index[12].name "ib"
+documenttype[0].index[12].field[0].name "ib"
+documenttype[0].index[13].name "ic"
+documenttype[0].index[13].field[0].name "ic"
+documenttype[0].index[14].name "nostemstring1"
+documenttype[0].index[14].field[0].name "nostemstring1"
+documenttype[0].index[15].name "nostemstring2"
+documenttype[0].index[15].field[0].name "nostemstring2"
+documenttype[0].index[16].name "nostemstring3"
+documenttype[0].index[16].field[0].name "nostemstring3"
+documenttype[0].index[17].name "nostemstring4"
+documenttype[0].index[17].field[0].name "nostemstring4"
+documenttype[0].index[18].name "fs9"
+documenttype[0].index[18].field[0].name "se"
+documenttype[0].index[19].name "f10"
+documenttype[0].index[19].field[0].name "f10.text"
+documenttype[0].index[20].name "f10.text"
+documenttype[0].index[20].field[0].name "f10.text"
+documenttype[0].index[21].name "sd_literal"
+documenttype[0].index[21].field[0].name "sd_literal"
+documenttype[0].index[22].name "pos_zcurve"
+documenttype[0].index[22].field[0].name "pos_zcurve"
+documenttype[0].index[23].name "fs1"
+documenttype[0].index[23].field[0].name "se"
+documenttype[0].index[23].field[1].name "sf"
+documenttype[0].index[23].field[2].name "sg"
+documenttype[0].index[24].name "fs2"
+documenttype[0].index[24].field[0].name "si"
+documenttype[0].index[25].name "fs3"
+documenttype[0].index[25].field[0].name "ib"
+documenttype[0].index[25].field[1].name "ic"
+documenttype[0].index[26].name "fs4"
+documenttype[0].index[26].field[0].name "sa"
+documenttype[0].index[26].field[1].name "sb"
+documenttype[0].index[27].name "onlyattrib"
+documenttype[0].index[27].field[0].name "se"
+documenttype[0].index[28].name "exactfields"
+documenttype[0].index[28].field[0].name "exact1"
+documenttype[0].index[28].field[1].name "exact2"
+documenttype[0].index[29].name "exactexplicit"
+documenttype[0].index[29].field[0].name "sa"
+documenttype[0].index[29].field[1].name "sb"
+documenttype[0].index[30].name "exactexplicit2"
+documenttype[0].index[30].field[0].name "sc"
+documenttype[0].index[30].field[1].name "sd"
+documenttype[0].index[31].name "gram"
+documenttype[0].index[31].field[0].name "sc"
+documenttype[0].index[31].field[1].name "sd"
+documenttype[0].index[32].name "nostem1"
+documenttype[0].index[32].field[0].name "nostemstring1"
+documenttype[0].index[32].field[1].name "nostemstring2"
+documenttype[0].index[33].name "nostem2"
+documenttype[0].index[33].field[0].name "nostemstring3"
+documenttype[0].index[33].field[1].name "nostemstring4"
+documenttype[0].index[34].name "default"
+documenttype[0].index[34].field[0].name "sb"
+documenttype[0].index[34].field[1].name "sc" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexswitches/attributes.cfg b/config-model/src/test/derived/indexswitches/attributes.cfg
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/attributes.cfg
diff --git a/config-model/src/test/derived/indexswitches/documentmanager.cfg b/config-model/src/test/derived/indexswitches/documentmanager.cfg
new file mode 100644
index 00000000000..0a32e670d7b
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/documentmanager.cfg
@@ -0,0 +1,50 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -555640823
+datatype[1].structtype[0].name "indexswitches.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "title"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[1].structtype[0].field[1].name "descr"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "source_src"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[1].structtype[0].field[3].name "source"
+datatype[1].structtype[0].field[3].datatype 2
+datatype[1].structtype[0].field[4].name "rankfeatures"
+datatype[1].structtype[0].field[4].datatype 2
+datatype[1].structtype[0].field[5].name "summaryfeatures"
+datatype[1].structtype[0].field[5].datatype 2
+datatype[2].id -1892617122
+datatype[2].structtype[0].name "indexswitches.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[3].id -753375626
+datatype[3].documenttype[0].name "indexswitches"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct -555640823
+datatype[3].documenttype[0].bodystruct -1892617122
+datatype[3].documenttype[0].fieldsets{default}.fields[0] "descr"
+datatype[3].documenttype[0].fieldsets{default}.fields[1] "title"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[0] "descr"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[1] "source_src"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[2] "title"
diff --git a/config-model/src/test/derived/indexswitches/ilscripts.cfg b/config-model/src/test/derived/indexswitches/ilscripts.cfg
new file mode 100644
index 00000000000..706f4105224
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/ilscripts.cfg
@@ -0,0 +1,9 @@
+maxtermoccurrences 100
+ilscript[0].doctype "indexswitches"
+ilscript[0].docfield[0] "title"
+ilscript[0].docfield[1] "descr"
+ilscript[0].docfield[2] "source_src"
+ilscript[0].content[0] "clear_state | guard { input source_src | switch { case \"theweb\": input source_src | tokenize normalize | summary source | index source; case \"amg\": input source_src | tokenize normalize | summary source; default: input source_src . \" partner\" | tokenize normalize | summary source | index source; }; }"
+ilscript[0].content[1] "clear_state | guard { input title | tokenize normalize stem:\"SHORTEST\" | summary title | index title; }"
+ilscript[0].content[2] "clear_state | guard { input descr | tokenize normalize stem:\"SHORTEST\" | summary descr | index descr; }"
+ilscript[0].content[3] "input source_src | passthrough source_src" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexswitches/index-info.cfg b/config-model/src/test/derived/indexswitches/index-info.cfg
new file mode 100644
index 00000000000..3daf23f8b67
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/index-info.cfg
@@ -0,0 +1,41 @@
+indexinfo[0].name "indexswitches"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "title"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "default"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "title"
+indexinfo[0].command[4].command "lowercase"
+indexinfo[0].command[5].indexname "default"
+indexinfo[0].command[5].command "lowercase"
+indexinfo[0].command[6].indexname "title"
+indexinfo[0].command[6].command "stem:SHORTEST"
+indexinfo[0].command[7].indexname "default"
+indexinfo[0].command[7].command "stem:SHORTEST"
+indexinfo[0].command[8].indexname "title"
+indexinfo[0].command[8].command "normalize"
+indexinfo[0].command[9].indexname "default"
+indexinfo[0].command[9].command "normalize"
+indexinfo[0].command[10].indexname "descr"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "descr"
+indexinfo[0].command[11].command "lowercase"
+indexinfo[0].command[12].indexname "descr"
+indexinfo[0].command[12].command "stem:SHORTEST"
+indexinfo[0].command[13].indexname "descr"
+indexinfo[0].command[13].command "normalize"
+indexinfo[0].command[14].indexname "source_src"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "rankfeatures"
+indexinfo[0].command[15].command "index"
+indexinfo[0].command[16].indexname "source"
+indexinfo[0].command[16].command "index"
+indexinfo[0].command[17].indexname "source"
+indexinfo[0].command[17].command "lowercase"
+indexinfo[0].command[18].indexname "source"
+indexinfo[0].command[18].command "normalize"
+indexinfo[0].command[19].indexname "summaryfeatures"
+indexinfo[0].command[19].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexswitches/indexswitches.sd b/config-model/src/test/derived/indexswitches/indexswitches.sd
new file mode 100644
index 00000000000..9b08a0c9386
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/indexswitches.sd
@@ -0,0 +1,32 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexswitches {
+
+ document indexswitches {
+
+ field title type string {
+ indexing: summary | index
+ }
+
+ field descr type string {
+ indexing: summary | index
+ }
+
+ field source_src type string {
+
+ }
+ }
+ field source type string {
+ indexing {
+ input source_src |
+ switch {
+ case "amg": input source_src | summary;
+ case "theweb": input source_src | summary | index;
+ default: input source_src . " partner" | summary | index;
+ };
+ }
+ stemming: none
+ }
+ fieldset default {
+ fields: title, descr
+ }
+}
diff --git a/config-model/src/test/derived/indexswitches/rank-profiles.cfg b/config-model/src/test/derived/indexswitches/rank-profiles.cfg
new file mode 100644
index 00000000000..caca83a9a91
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/rank-profiles.cfg
@@ -0,0 +1,10 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexswitches/summary.cfg b/config-model/src/test/derived/indexswitches/summary.cfg
new file mode 100644
index 00000000000..8644ad1262b
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/summary.cfg
@@ -0,0 +1,15 @@
+defaultsummaryid 1698765342
+classes[0].id 1698765342
+classes[0].name "default"
+classes[0].fields[0].name "source"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "title"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "descr"
+classes[0].fields[2].type "longstring"
+classes[0].fields[3].name "rankfeatures"
+classes[0].fields[3].type "featuredata"
+classes[0].fields[4].name "summaryfeatures"
+classes[0].fields[4].type "featuredata"
+classes[0].fields[5].name "documentid"
+classes[0].fields[5].type "longstring" \ No newline at end of file
diff --git a/config-model/src/test/derived/indexswitches/summarymap.cfg b/config-model/src/test/derived/indexswitches/summarymap.cfg
new file mode 100644
index 00000000000..42b6e811ee6
--- /dev/null
+++ b/config-model/src/test/derived/indexswitches/summarymap.cfg
@@ -0,0 +1,7 @@
+defaultoutputclass -1
+override[0].field "rankfeatures"
+override[0].command "rankfeatures"
+override[0].arguments ""
+override[1].field "summaryfeatures"
+override[1].command "summaryfeatures"
+override[1].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/inheritance/attributes.cfg b/config-model/src/test/derived/inheritance/attributes.cfg
new file mode 100644
index 00000000000..b9870b21975
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/attributes.cfg
@@ -0,0 +1,10 @@
+attribute[3]
+attribute[onlygrandparent].collectiontype SINGLE
+attribute[onlygrandparent].datatype INT32
+attribute[onlygrandparent].name "onlygrandparent"
+attribute[onlymother].collectiontype SINGLE
+attribute[onlymother].datatype STRING
+attribute[onlymother].name "onlymother"
+attribute[overridden].collectiontype SINGLE
+attribute[overridden].datatype INT32
+attribute[overridden].name "overridden"
diff --git a/config-model/src/test/derived/inheritance/child.sd b/config-model/src/test/derived/inheritance/child.sd
new file mode 100644
index 00000000000..099e9cab0ee
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/child.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+
+ document child inherits father, mother {
+
+ field onlychild type string {
+ indexing: index
+ }
+
+ field overridden type int {
+ indexing: index
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/inheritance/documentmanager.cfg b/config-model/src/test/derived/inheritance/documentmanager.cfg
new file mode 100644
index 00000000000..21e65acf67d
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/documentmanager.cfg
@@ -0,0 +1,426 @@
+enablecompression false
+annotationtype[0]
+datatype[29]
+datatype[-126593034].id -126593034
+datatype[-126593034].annotationreftype[0]
+datatype[-126593034].arraytype[0]
+datatype[-126593034].documenttype[0]
+datatype[-126593034].structtype[1]
+datatype[-126593034].structtype[single].name "child.body"
+datatype[-126593034].structtype[single].version 0
+datatype[-126593034].structtype[single].field[0]
+datatype[-126593034].structtype[single].inherits[0]
+datatype[-126593034].weightedsettype[0]
+datatype[-141935690].id -141935690
+datatype[-141935690].annotationreftype[0]
+datatype[-141935690].arraytype[0]
+datatype[-141935690].documenttype[0]
+datatype[-141935690].structtype[1]
+datatype[-141935690].structtype[single].name "search_smartsummary"
+datatype[-141935690].structtype[single].version 0
+datatype[-141935690].structtype[single].field[3]
+datatype[-141935690].structtype[single].field[abstract].datatype 2
+datatype[-141935690].structtype[single].field[abstract].name "abstract"
+datatype[-141935690].structtype[single].field[abstract].id[0]
+datatype[-141935690].structtype[single].field[dispurl].datatype 2
+datatype[-141935690].structtype[single].field[dispurl].name "dispurl"
+datatype[-141935690].structtype[single].field[dispurl].id[0]
+datatype[-141935690].structtype[single].field[title].datatype 2
+datatype[-141935690].structtype[single].field[title].name "title"
+datatype[-141935690].structtype[single].field[title].id[0]
+datatype[-141935690].structtype[single].inherits[0]
+datatype[-141935690].weightedsettype[0]
+datatype[-1467672569].id -1467672569
+datatype[-1467672569].annotationreftype[0]
+datatype[-1467672569].arraytype[0]
+datatype[-1467672569].documenttype[0]
+datatype[-1467672569].structtype[1]
+datatype[-1467672569].structtype[single].name "child_search.body"
+datatype[-1467672569].structtype[single].version 0
+datatype[-1467672569].structtype[single].field[0]
+datatype[-1467672569].structtype[single].inherits[0]
+datatype[-1467672569].weightedsettype[0]
+datatype[-154107656].id -154107656
+datatype[-154107656].annotationreftype[0]
+datatype[-154107656].arraytype[0]
+datatype[-154107656].documenttype[1]
+datatype[-154107656].documenttype[single].bodystruct 978262812
+datatype[-154107656].documenttype[single].headerstruct 990971719
+datatype[-154107656].documenttype[single].name "grandparent"
+datatype[-154107656].documenttype[single].version 0
+datatype[-154107656].documenttype[single].inherits[0]
+datatype[-154107656].structtype[0]
+datatype[-154107656].weightedsettype[0]
+datatype[-158393403].id -158393403
+datatype[-158393403].annotationreftype[0]
+datatype[-158393403].arraytype[0]
+datatype[-158393403].documenttype[1]
+datatype[-158393403].documenttype[single].bodystruct -1989003153
+datatype[-158393403].documenttype[single].headerstruct 1306663898
+datatype[-158393403].documenttype[single].name "mother"
+datatype[-158393403].documenttype[single].version 0
+datatype[-158393403].documenttype[single].inherits[1]
+datatype[-158393403].documenttype[single].inherits[grandparent].name "grandparent"
+datatype[-158393403].documenttype[single].inherits[grandparent].version 0
+datatype[-158393403].structtype[0]
+datatype[-158393403].weightedsettype[0]
+datatype[-1740240543].id -1740240543
+datatype[-1740240543].annotationreftype[0]
+datatype[-1740240543].arraytype[0]
+datatype[-1740240543].documenttype[0]
+datatype[-1740240543].structtype[1]
+datatype[-1740240543].structtype[single].name "search_feature"
+datatype[-1740240543].structtype[single].version 0
+datatype[-1740240543].structtype[single].field[2]
+datatype[-1740240543].structtype[single].field[name].datatype 2
+datatype[-1740240543].structtype[single].field[name].name "name"
+datatype[-1740240543].structtype[single].field[name].id[0]
+datatype[-1740240543].structtype[single].field[value].datatype 5
+datatype[-1740240543].structtype[single].field[value].name "value"
+datatype[-1740240543].structtype[single].field[value].id[0]
+datatype[-1740240543].structtype[single].inherits[0]
+datatype[-1740240543].weightedsettype[0]
+datatype[-1742340170].id -1742340170
+datatype[-1742340170].annotationreftype[0]
+datatype[-1742340170].arraytype[0]
+datatype[-1742340170].documenttype[0]
+datatype[-1742340170].structtype[1]
+datatype[-1742340170].structtype[single].name "father.body"
+datatype[-1742340170].structtype[single].version 0
+datatype[-1742340170].structtype[single].field[0]
+datatype[-1742340170].structtype[single].inherits[0]
+datatype[-1742340170].weightedsettype[0]
+datatype[-1852215954].id -1852215954
+datatype[-1852215954].annotationreftype[0]
+datatype[-1852215954].arraytype[0]
+datatype[-1852215954].documenttype[0]
+datatype[-1852215954].structtype[1]
+datatype[-1852215954].structtype[single].name "mother_search.body"
+datatype[-1852215954].structtype[single].version 0
+datatype[-1852215954].structtype[single].field[0]
+datatype[-1852215954].structtype[single].inherits[0]
+datatype[-1852215954].weightedsettype[0]
+datatype[-1962244686].id -1962244686
+datatype[-1962244686].annotationreftype[0]
+datatype[-1962244686].arraytype[0]
+datatype[-1962244686].documenttype[0]
+datatype[-1962244686].structtype[1]
+datatype[-1962244686].structtype[single].name "father_search.header"
+datatype[-1962244686].structtype[single].version 0
+datatype[-1962244686].structtype[single].field[5]
+datatype[-1962244686].structtype[single].field[onlyfather].datatype 2
+datatype[-1962244686].structtype[single].field[onlyfather].name "onlyfather"
+datatype[-1962244686].structtype[single].field[onlyfather].id[0]
+datatype[-1962244686].structtype[single].field[onlygrandparent].datatype 0
+datatype[-1962244686].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[-1962244686].structtype[single].field[onlygrandparent].id[0]
+datatype[-1962244686].structtype[single].field[overridden].datatype 0
+datatype[-1962244686].structtype[single].field[overridden].name "overridden"
+datatype[-1962244686].structtype[single].field[overridden].id[0]
+datatype[-1962244686].structtype[single].field[rankfeatures].datatype 147991900
+datatype[-1962244686].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[-1962244686].structtype[single].field[rankfeatures].id[0]
+datatype[-1962244686].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[-1962244686].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[-1962244686].structtype[single].field[summaryfeatures].id[0]
+datatype[-1962244686].structtype[single].inherits[0]
+datatype[-1962244686].weightedsettype[0]
+datatype[-1989003153].id -1989003153
+datatype[-1989003153].annotationreftype[0]
+datatype[-1989003153].arraytype[0]
+datatype[-1989003153].documenttype[0]
+datatype[-1989003153].structtype[1]
+datatype[-1989003153].structtype[single].name "mother.body"
+datatype[-1989003153].structtype[single].version 0
+datatype[-1989003153].structtype[single].field[0]
+datatype[-1989003153].structtype[single].inherits[0]
+datatype[-1989003153].weightedsettype[0]
+datatype[-205818510].id -205818510
+datatype[-205818510].annotationreftype[0]
+datatype[-205818510].arraytype[0]
+datatype[-205818510].documenttype[0]
+datatype[-205818510].structtype[1]
+datatype[-205818510].structtype[single].name "child_search.header"
+datatype[-205818510].structtype[single].version 0
+datatype[-205818510].structtype[single].field[7]
+datatype[-205818510].structtype[single].field[onlychild].datatype 2
+datatype[-205818510].structtype[single].field[onlychild].name "onlychild"
+datatype[-205818510].structtype[single].field[onlychild].id[0]
+datatype[-205818510].structtype[single].field[onlyfather].datatype 2
+datatype[-205818510].structtype[single].field[onlyfather].name "onlyfather"
+datatype[-205818510].structtype[single].field[onlyfather].id[0]
+datatype[-205818510].structtype[single].field[onlygrandparent].datatype 0
+datatype[-205818510].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[-205818510].structtype[single].field[onlygrandparent].id[0]
+datatype[-205818510].structtype[single].field[onlymother].datatype 2
+datatype[-205818510].structtype[single].field[onlymother].name "onlymother"
+datatype[-205818510].structtype[single].field[onlymother].id[0]
+datatype[-205818510].structtype[single].field[overridden].datatype 0
+datatype[-205818510].structtype[single].field[overridden].name "overridden"
+datatype[-205818510].structtype[single].field[overridden].id[0]
+datatype[-205818510].structtype[single].field[rankfeatures].datatype 147991900
+datatype[-205818510].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[-205818510].structtype[single].field[rankfeatures].id[0]
+datatype[-205818510].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[-205818510].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[-205818510].structtype[single].field[summaryfeatures].id[0]
+datatype[-205818510].structtype[single].inherits[0]
+datatype[-205818510].weightedsettype[0]
+datatype[-384824039].id -384824039
+datatype[-384824039].annotationreftype[0]
+datatype[-384824039].arraytype[0]
+datatype[-384824039].documenttype[0]
+datatype[-384824039].structtype[1]
+datatype[-384824039].structtype[single].name "mother_search.header"
+datatype[-384824039].structtype[single].version 0
+datatype[-384824039].structtype[single].field[5]
+datatype[-384824039].structtype[single].field[onlygrandparent].datatype 0
+datatype[-384824039].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[-384824039].structtype[single].field[onlygrandparent].id[0]
+datatype[-384824039].structtype[single].field[onlymother].datatype 2
+datatype[-384824039].structtype[single].field[onlymother].name "onlymother"
+datatype[-384824039].structtype[single].field[onlymother].id[0]
+datatype[-384824039].structtype[single].field[overridden].datatype 0
+datatype[-384824039].structtype[single].field[overridden].name "overridden"
+datatype[-384824039].structtype[single].field[overridden].id[0]
+datatype[-384824039].structtype[single].field[rankfeatures].datatype 147991900
+datatype[-384824039].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[-384824039].structtype[single].field[rankfeatures].id[0]
+datatype[-384824039].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[-384824039].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[-384824039].structtype[single].field[summaryfeatures].id[0]
+datatype[-384824039].structtype[single].inherits[0]
+datatype[-384824039].weightedsettype[0]
+datatype[-52742073].id -52742073
+datatype[-52742073].annotationreftype[0]
+datatype[-52742073].arraytype[0]
+datatype[-52742073].documenttype[0]
+datatype[-52742073].structtype[1]
+datatype[-52742073].structtype[single].name "father_search.body"
+datatype[-52742073].structtype[single].version 0
+datatype[-52742073].structtype[single].field[0]
+datatype[-52742073].structtype[single].inherits[0]
+datatype[-52742073].weightedsettype[0]
+datatype[-580592339].id -580592339
+datatype[-580592339].annotationreftype[0]
+datatype[-580592339].arraytype[0]
+datatype[-580592339].documenttype[1]
+datatype[-580592339].documenttype[single].bodystruct -1467672569
+datatype[-580592339].documenttype[single].headerstruct -205818510
+datatype[-580592339].documenttype[single].name "child_search"
+datatype[-580592339].documenttype[single].version 0
+datatype[-580592339].documenttype[single].inherits[0]
+datatype[-580592339].structtype[0]
+datatype[-580592339].weightedsettype[0]
+datatype[-876064862].id -876064862
+datatype[-876064862].annotationreftype[0]
+datatype[-876064862].arraytype[0]
+datatype[-876064862].documenttype[0]
+datatype[-876064862].structtype[1]
+datatype[-876064862].structtype[single].name "search_position"
+datatype[-876064862].structtype[single].version 0
+datatype[-876064862].structtype[single].field[2]
+datatype[-876064862].structtype[single].field[x].datatype 0
+datatype[-876064862].structtype[single].field[x].name "x"
+datatype[-876064862].structtype[single].field[x].id[0]
+datatype[-876064862].structtype[single].field[y].datatype 0
+datatype[-876064862].structtype[single].field[y].name "y"
+datatype[-876064862].structtype[single].field[y].id[0]
+datatype[-876064862].structtype[single].inherits[0]
+datatype[-876064862].weightedsettype[0]
+datatype[1306663898].id 1306663898
+datatype[1306663898].annotationreftype[0]
+datatype[1306663898].arraytype[0]
+datatype[1306663898].documenttype[0]
+datatype[1306663898].structtype[1]
+datatype[1306663898].structtype[single].name "mother.header"
+datatype[1306663898].structtype[single].version 0
+datatype[1306663898].structtype[single].field[1]
+datatype[1306663898].structtype[single].field[onlymother].datatype 2
+datatype[1306663898].structtype[single].field[onlymother].name "onlymother"
+datatype[1306663898].structtype[single].field[onlymother].id[0]
+datatype[1306663898].structtype[single].inherits[0]
+datatype[1306663898].weightedsettype[0]
+datatype[1464571117].id 1464571117
+datatype[1464571117].annotationreftype[0]
+datatype[1464571117].arraytype[0]
+datatype[1464571117].documenttype[1]
+datatype[1464571117].documenttype[single].bodystruct -52742073
+datatype[1464571117].documenttype[single].headerstruct -1962244686
+datatype[1464571117].documenttype[single].name "father_search"
+datatype[1464571117].documenttype[single].version 0
+datatype[1464571117].documenttype[single].inherits[0]
+datatype[1464571117].structtype[0]
+datatype[1464571117].weightedsettype[0]
+datatype[147991900].id 147991900
+datatype[147991900].annotationreftype[0]
+datatype[147991900].arraytype[1]
+datatype[147991900].arraytype[single].datatype -1740240543
+datatype[147991900].documenttype[0]
+datatype[147991900].structtype[0]
+datatype[147991900].weightedsettype[0]
+datatype[1530060044].id 1530060044
+datatype[1530060044].annotationreftype[0]
+datatype[1530060044].arraytype[0]
+datatype[1530060044].documenttype[0]
+datatype[1530060044].structtype[1]
+datatype[1530060044].structtype[single].name "grandparent_search.header"
+datatype[1530060044].structtype[single].version 0
+datatype[1530060044].structtype[single].field[4]
+datatype[1530060044].structtype[single].field[onlygrandparent].datatype 0
+datatype[1530060044].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[1530060044].structtype[single].field[onlygrandparent].id[0]
+datatype[1530060044].structtype[single].field[overridden].datatype 0
+datatype[1530060044].structtype[single].field[overridden].name "overridden"
+datatype[1530060044].structtype[single].field[overridden].id[0]
+datatype[1530060044].structtype[single].field[rankfeatures].datatype 147991900
+datatype[1530060044].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[1530060044].structtype[single].field[rankfeatures].id[0]
+datatype[1530060044].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[1530060044].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[1530060044].structtype[single].field[summaryfeatures].id[0]
+datatype[1530060044].structtype[single].inherits[0]
+datatype[1530060044].weightedsettype[0]
+datatype[1845861921].id 1845861921
+datatype[1845861921].annotationreftype[0]
+datatype[1845861921].arraytype[0]
+datatype[1845861921].documenttype[0]
+datatype[1845861921].structtype[1]
+datatype[1845861921].structtype[single].name "grandparent_search.body"
+datatype[1845861921].structtype[single].version 0
+datatype[1845861921].structtype[single].field[0]
+datatype[1845861921].structtype[single].inherits[0]
+datatype[1845861921].weightedsettype[0]
+datatype[2126589281].id 2126589281
+datatype[2126589281].annotationreftype[0]
+datatype[2126589281].arraytype[0]
+datatype[2126589281].documenttype[0]
+datatype[2126589281].structtype[1]
+datatype[2126589281].structtype[single].name "father.header"
+datatype[2126589281].structtype[single].version 0
+datatype[2126589281].structtype[single].field[1]
+datatype[2126589281].structtype[single].field[onlyfather].datatype 2
+datatype[2126589281].structtype[single].field[onlyfather].name "onlyfather"
+datatype[2126589281].structtype[single].field[onlyfather].id[0]
+datatype[2126589281].structtype[single].inherits[0]
+datatype[2126589281].weightedsettype[0]
+datatype[328953555].id 328953555
+datatype[328953555].annotationreftype[0]
+datatype[328953555].arraytype[0]
+datatype[328953555].documenttype[1]
+datatype[328953555].documenttype[single].bodystruct 1845861921
+datatype[328953555].documenttype[single].headerstruct 1530060044
+datatype[328953555].documenttype[single].name "grandparent_search"
+datatype[328953555].documenttype[single].version 0
+datatype[328953555].documenttype[single].inherits[0]
+datatype[328953555].structtype[0]
+datatype[328953555].weightedsettype[0]
+datatype[464784087].id 464784087
+datatype[464784087].annotationreftype[0]
+datatype[464784087].arraytype[0]
+datatype[464784087].documenttype[0]
+datatype[464784087].structtype[1]
+datatype[464784087].structtype[single].name "search_uri"
+datatype[464784087].structtype[single].version 0
+datatype[464784087].structtype[single].field[7]
+datatype[464784087].structtype[single].field[all].datatype 2
+datatype[464784087].structtype[single].field[all].name "all"
+datatype[464784087].structtype[single].field[all].id[0]
+datatype[464784087].structtype[single].field[fragment].datatype 2
+datatype[464784087].structtype[single].field[fragment].name "fragment"
+datatype[464784087].structtype[single].field[fragment].id[0]
+datatype[464784087].structtype[single].field[host].datatype 2
+datatype[464784087].structtype[single].field[host].name "host"
+datatype[464784087].structtype[single].field[host].id[0]
+datatype[464784087].structtype[single].field[path].datatype 2
+datatype[464784087].structtype[single].field[path].name "path"
+datatype[464784087].structtype[single].field[path].id[0]
+datatype[464784087].structtype[single].field[port].datatype 0
+datatype[464784087].structtype[single].field[port].name "port"
+datatype[464784087].structtype[single].field[port].id[0]
+datatype[464784087].structtype[single].field[query].datatype 2
+datatype[464784087].structtype[single].field[query].name "query"
+datatype[464784087].structtype[single].field[query].id[0]
+datatype[464784087].structtype[single].field[scheme].datatype 2
+datatype[464784087].structtype[single].field[scheme].name "scheme"
+datatype[464784087].structtype[single].field[scheme].id[0]
+datatype[464784087].structtype[single].inherits[0]
+datatype[464784087].weightedsettype[0]
+datatype[644645734].id 644645734
+datatype[644645734].annotationreftype[0]
+datatype[644645734].arraytype[0]
+datatype[644645734].documenttype[1]
+datatype[644645734].documenttype[single].bodystruct -1852215954
+datatype[644645734].documenttype[single].headerstruct -384824039
+datatype[644645734].documenttype[single].name "mother_search"
+datatype[644645734].documenttype[single].version 0
+datatype[644645734].documenttype[single].inherits[0]
+datatype[644645734].structtype[0]
+datatype[644645734].weightedsettype[0]
+datatype[746267614].id 746267614
+datatype[746267614].annotationreftype[0]
+datatype[746267614].arraytype[0]
+datatype[746267614].documenttype[1]
+datatype[746267614].documenttype[single].bodystruct -126593034
+datatype[746267614].documenttype[single].headerstruct 81425825
+datatype[746267614].documenttype[single].name "child"
+datatype[746267614].documenttype[single].version 0
+datatype[746267614].documenttype[single].inherits[2]
+datatype[746267614].documenttype[single].inherits[father].name "father"
+datatype[746267614].documenttype[single].inherits[father].version 0
+datatype[746267614].documenttype[single].inherits[mother].name "mother"
+datatype[746267614].documenttype[single].inherits[mother].version 0
+datatype[746267614].structtype[0]
+datatype[746267614].weightedsettype[0]
+datatype[81425825].id 81425825
+datatype[81425825].annotationreftype[0]
+datatype[81425825].arraytype[0]
+datatype[81425825].documenttype[0]
+datatype[81425825].structtype[1]
+datatype[81425825].structtype[single].name "child.header"
+datatype[81425825].structtype[single].version 0
+datatype[81425825].structtype[single].field[1]
+datatype[81425825].structtype[single].field[onlychild].datatype 2
+datatype[81425825].structtype[single].field[onlychild].name "onlychild"
+datatype[81425825].structtype[single].field[onlychild].id[0]
+datatype[81425825].structtype[single].inherits[0]
+datatype[81425825].weightedsettype[0]
+datatype[978262812].id 978262812
+datatype[978262812].annotationreftype[0]
+datatype[978262812].arraytype[0]
+datatype[978262812].documenttype[0]
+datatype[978262812].structtype[1]
+datatype[978262812].structtype[single].name "grandparent.body"
+datatype[978262812].structtype[single].version 0
+datatype[978262812].structtype[single].field[0]
+datatype[978262812].structtype[single].inherits[0]
+datatype[978262812].weightedsettype[0]
+datatype[986686494].id 986686494
+datatype[986686494].annotationreftype[0]
+datatype[986686494].arraytype[0]
+datatype[986686494].documenttype[1]
+datatype[986686494].documenttype[single].bodystruct -1742340170
+datatype[986686494].documenttype[single].headerstruct 2126589281
+datatype[986686494].documenttype[single].name "father"
+datatype[986686494].documenttype[single].version 0
+datatype[986686494].documenttype[single].inherits[1]
+datatype[986686494].documenttype[single].inherits[grandparent].name "grandparent"
+datatype[986686494].documenttype[single].inherits[grandparent].version 0
+datatype[986686494].structtype[0]
+datatype[986686494].weightedsettype[0]
+datatype[990971719].id 990971719
+datatype[990971719].annotationreftype[0]
+datatype[990971719].arraytype[0]
+datatype[990971719].documenttype[0]
+datatype[990971719].structtype[1]
+datatype[990971719].structtype[single].name "grandparent.header"
+datatype[990971719].structtype[single].version 0
+datatype[990971719].structtype[single].field[2]
+datatype[990971719].structtype[single].field[onlygrandparent].datatype 0
+datatype[990971719].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[990971719].structtype[single].field[onlygrandparent].id[0]
+datatype[990971719].structtype[single].field[overridden].datatype 0
+datatype[990971719].structtype[single].field[overridden].name "overridden"
+datatype[990971719].structtype[single].field[overridden].id[0]
+datatype[990971719].structtype[single].inherits[0]
+datatype[990971719].weightedsettype[0]
diff --git a/config-model/src/test/derived/inheritance/father.sd b/config-model/src/test/derived/inheritance/father.sd
new file mode 100644
index 00000000000..de4287334c2
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/father.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document father inherits grandparent {
+
+ field onlyfather type string {
+ indexing: summary
+ }
+
+ field overridden type int {
+ indexing: summary
+ }
+
+}
diff --git a/config-model/src/test/derived/inheritance/grandparent.sd b/config-model/src/test/derived/inheritance/grandparent.sd
new file mode 100644
index 00000000000..5f1aa91b926
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/grandparent.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document grandparent {
+
+ field onlygrandparent type int {
+ indexing: attribute
+ }
+
+ field overridden type int {
+ indexing: attribute
+ }
+
+}
diff --git a/config-model/src/test/derived/inheritance/ilscripts.cfg b/config-model/src/test/derived/inheritance/ilscripts.cfg
new file mode 100644
index 00000000000..8a9f8fe3c6f
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/ilscripts.cfg
@@ -0,0 +1,9 @@
+ilscript[1]
+ilscript[child].doctype "child"
+ilscript[child].name "child"
+ilscript[child].content[5]
+ilscript[child].content[0] "input onlygrandparent | attribute onlygrandparent"
+ilscript[child].content[1] "input overridden | attribute overridden"
+ilscript[child].content[2] "input onlyfather | summary onlyfather"
+ilscript[child].content[3] "input onlymother | tokenize normalize stem:\"SHORTEST\" | attribute onlymother | index onlymother"
+ilscript[child].content[4] "input onlychild | tokenize normalize stem:\"SHORTEST\" | index onlychild"
diff --git a/config-model/src/test/derived/inheritance/index-info.cfg b/config-model/src/test/derived/inheritance/index-info.cfg
new file mode 100644
index 00000000000..f58df991c40
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/index-info.cfg
@@ -0,0 +1,30 @@
+indexinfo[1]
+indexinfo[child].name "child"
+indexinfo[child].alias[0]
+indexinfo[child].command[13]
+indexinfo[child].command[00].command "index"
+indexinfo[child].command[00].indexname "sddocname"
+indexinfo[child].command[01].command "word"
+indexinfo[child].command[01].indexname "sddocname"
+indexinfo[child].command[02].command "index"
+indexinfo[child].command[02].indexname "onlychild"
+indexinfo[child].command[03].command "index"
+indexinfo[child].command[03].indexname "onlygrandparent"
+indexinfo[child].command[04].command "attribute"
+indexinfo[child].command[04].indexname "onlygrandparent"
+indexinfo[child].command[05].command "index"
+indexinfo[child].command[05].indexname "overridden"
+indexinfo[child].command[06].command "attribute"
+indexinfo[child].command[06].indexname "overridden"
+indexinfo[child].command[07].command "index"
+indexinfo[child].command[07].indexname "onlyfather"
+indexinfo[child].command[08].command "index"
+indexinfo[child].command[08].indexname "onlymother"
+indexinfo[child].command[09].command "stem:SHORTEST"
+indexinfo[child].command[09].indexname "onlymother"
+indexinfo[child].command[10].command "normalize"
+indexinfo[child].command[10].indexname "onlymother"
+indexinfo[child].command[11].command "stem:SHORTEST"
+indexinfo[child].command[11].indexname "onlychild"
+indexinfo[child].command[12].command "normalize"
+indexinfo[child].command[12].indexname "onlychild"
diff --git a/config-model/src/test/derived/inheritance/mother.sd b/config-model/src/test/derived/inheritance/mother.sd
new file mode 100644
index 00000000000..437616eabbc
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/mother.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document mother inherits grandparent {
+
+ field onlymother type string {
+ indexing: attribute | index
+ index: prefix
+ }
+
+ field overridden type int {
+ indexing: attribute
+ }
+
+}
diff --git a/config-model/src/test/derived/inheritance/mother/documentmanager.cfg b/config-model/src/test/derived/inheritance/mother/documentmanager.cfg
new file mode 100644
index 00000000000..21e65acf67d
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/mother/documentmanager.cfg
@@ -0,0 +1,426 @@
+enablecompression false
+annotationtype[0]
+datatype[29]
+datatype[-126593034].id -126593034
+datatype[-126593034].annotationreftype[0]
+datatype[-126593034].arraytype[0]
+datatype[-126593034].documenttype[0]
+datatype[-126593034].structtype[1]
+datatype[-126593034].structtype[single].name "child.body"
+datatype[-126593034].structtype[single].version 0
+datatype[-126593034].structtype[single].field[0]
+datatype[-126593034].structtype[single].inherits[0]
+datatype[-126593034].weightedsettype[0]
+datatype[-141935690].id -141935690
+datatype[-141935690].annotationreftype[0]
+datatype[-141935690].arraytype[0]
+datatype[-141935690].documenttype[0]
+datatype[-141935690].structtype[1]
+datatype[-141935690].structtype[single].name "search_smartsummary"
+datatype[-141935690].structtype[single].version 0
+datatype[-141935690].structtype[single].field[3]
+datatype[-141935690].structtype[single].field[abstract].datatype 2
+datatype[-141935690].structtype[single].field[abstract].name "abstract"
+datatype[-141935690].structtype[single].field[abstract].id[0]
+datatype[-141935690].structtype[single].field[dispurl].datatype 2
+datatype[-141935690].structtype[single].field[dispurl].name "dispurl"
+datatype[-141935690].structtype[single].field[dispurl].id[0]
+datatype[-141935690].structtype[single].field[title].datatype 2
+datatype[-141935690].structtype[single].field[title].name "title"
+datatype[-141935690].structtype[single].field[title].id[0]
+datatype[-141935690].structtype[single].inherits[0]
+datatype[-141935690].weightedsettype[0]
+datatype[-1467672569].id -1467672569
+datatype[-1467672569].annotationreftype[0]
+datatype[-1467672569].arraytype[0]
+datatype[-1467672569].documenttype[0]
+datatype[-1467672569].structtype[1]
+datatype[-1467672569].structtype[single].name "child_search.body"
+datatype[-1467672569].structtype[single].version 0
+datatype[-1467672569].structtype[single].field[0]
+datatype[-1467672569].structtype[single].inherits[0]
+datatype[-1467672569].weightedsettype[0]
+datatype[-154107656].id -154107656
+datatype[-154107656].annotationreftype[0]
+datatype[-154107656].arraytype[0]
+datatype[-154107656].documenttype[1]
+datatype[-154107656].documenttype[single].bodystruct 978262812
+datatype[-154107656].documenttype[single].headerstruct 990971719
+datatype[-154107656].documenttype[single].name "grandparent"
+datatype[-154107656].documenttype[single].version 0
+datatype[-154107656].documenttype[single].inherits[0]
+datatype[-154107656].structtype[0]
+datatype[-154107656].weightedsettype[0]
+datatype[-158393403].id -158393403
+datatype[-158393403].annotationreftype[0]
+datatype[-158393403].arraytype[0]
+datatype[-158393403].documenttype[1]
+datatype[-158393403].documenttype[single].bodystruct -1989003153
+datatype[-158393403].documenttype[single].headerstruct 1306663898
+datatype[-158393403].documenttype[single].name "mother"
+datatype[-158393403].documenttype[single].version 0
+datatype[-158393403].documenttype[single].inherits[1]
+datatype[-158393403].documenttype[single].inherits[grandparent].name "grandparent"
+datatype[-158393403].documenttype[single].inherits[grandparent].version 0
+datatype[-158393403].structtype[0]
+datatype[-158393403].weightedsettype[0]
+datatype[-1740240543].id -1740240543
+datatype[-1740240543].annotationreftype[0]
+datatype[-1740240543].arraytype[0]
+datatype[-1740240543].documenttype[0]
+datatype[-1740240543].structtype[1]
+datatype[-1740240543].structtype[single].name "search_feature"
+datatype[-1740240543].structtype[single].version 0
+datatype[-1740240543].structtype[single].field[2]
+datatype[-1740240543].structtype[single].field[name].datatype 2
+datatype[-1740240543].structtype[single].field[name].name "name"
+datatype[-1740240543].structtype[single].field[name].id[0]
+datatype[-1740240543].structtype[single].field[value].datatype 5
+datatype[-1740240543].structtype[single].field[value].name "value"
+datatype[-1740240543].structtype[single].field[value].id[0]
+datatype[-1740240543].structtype[single].inherits[0]
+datatype[-1740240543].weightedsettype[0]
+datatype[-1742340170].id -1742340170
+datatype[-1742340170].annotationreftype[0]
+datatype[-1742340170].arraytype[0]
+datatype[-1742340170].documenttype[0]
+datatype[-1742340170].structtype[1]
+datatype[-1742340170].structtype[single].name "father.body"
+datatype[-1742340170].structtype[single].version 0
+datatype[-1742340170].structtype[single].field[0]
+datatype[-1742340170].structtype[single].inherits[0]
+datatype[-1742340170].weightedsettype[0]
+datatype[-1852215954].id -1852215954
+datatype[-1852215954].annotationreftype[0]
+datatype[-1852215954].arraytype[0]
+datatype[-1852215954].documenttype[0]
+datatype[-1852215954].structtype[1]
+datatype[-1852215954].structtype[single].name "mother_search.body"
+datatype[-1852215954].structtype[single].version 0
+datatype[-1852215954].structtype[single].field[0]
+datatype[-1852215954].structtype[single].inherits[0]
+datatype[-1852215954].weightedsettype[0]
+datatype[-1962244686].id -1962244686
+datatype[-1962244686].annotationreftype[0]
+datatype[-1962244686].arraytype[0]
+datatype[-1962244686].documenttype[0]
+datatype[-1962244686].structtype[1]
+datatype[-1962244686].structtype[single].name "father_search.header"
+datatype[-1962244686].structtype[single].version 0
+datatype[-1962244686].structtype[single].field[5]
+datatype[-1962244686].structtype[single].field[onlyfather].datatype 2
+datatype[-1962244686].structtype[single].field[onlyfather].name "onlyfather"
+datatype[-1962244686].structtype[single].field[onlyfather].id[0]
+datatype[-1962244686].structtype[single].field[onlygrandparent].datatype 0
+datatype[-1962244686].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[-1962244686].structtype[single].field[onlygrandparent].id[0]
+datatype[-1962244686].structtype[single].field[overridden].datatype 0
+datatype[-1962244686].structtype[single].field[overridden].name "overridden"
+datatype[-1962244686].structtype[single].field[overridden].id[0]
+datatype[-1962244686].structtype[single].field[rankfeatures].datatype 147991900
+datatype[-1962244686].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[-1962244686].structtype[single].field[rankfeatures].id[0]
+datatype[-1962244686].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[-1962244686].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[-1962244686].structtype[single].field[summaryfeatures].id[0]
+datatype[-1962244686].structtype[single].inherits[0]
+datatype[-1962244686].weightedsettype[0]
+datatype[-1989003153].id -1989003153
+datatype[-1989003153].annotationreftype[0]
+datatype[-1989003153].arraytype[0]
+datatype[-1989003153].documenttype[0]
+datatype[-1989003153].structtype[1]
+datatype[-1989003153].structtype[single].name "mother.body"
+datatype[-1989003153].structtype[single].version 0
+datatype[-1989003153].structtype[single].field[0]
+datatype[-1989003153].structtype[single].inherits[0]
+datatype[-1989003153].weightedsettype[0]
+datatype[-205818510].id -205818510
+datatype[-205818510].annotationreftype[0]
+datatype[-205818510].arraytype[0]
+datatype[-205818510].documenttype[0]
+datatype[-205818510].structtype[1]
+datatype[-205818510].structtype[single].name "child_search.header"
+datatype[-205818510].structtype[single].version 0
+datatype[-205818510].structtype[single].field[7]
+datatype[-205818510].structtype[single].field[onlychild].datatype 2
+datatype[-205818510].structtype[single].field[onlychild].name "onlychild"
+datatype[-205818510].structtype[single].field[onlychild].id[0]
+datatype[-205818510].structtype[single].field[onlyfather].datatype 2
+datatype[-205818510].structtype[single].field[onlyfather].name "onlyfather"
+datatype[-205818510].structtype[single].field[onlyfather].id[0]
+datatype[-205818510].structtype[single].field[onlygrandparent].datatype 0
+datatype[-205818510].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[-205818510].structtype[single].field[onlygrandparent].id[0]
+datatype[-205818510].structtype[single].field[onlymother].datatype 2
+datatype[-205818510].structtype[single].field[onlymother].name "onlymother"
+datatype[-205818510].structtype[single].field[onlymother].id[0]
+datatype[-205818510].structtype[single].field[overridden].datatype 0
+datatype[-205818510].structtype[single].field[overridden].name "overridden"
+datatype[-205818510].structtype[single].field[overridden].id[0]
+datatype[-205818510].structtype[single].field[rankfeatures].datatype 147991900
+datatype[-205818510].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[-205818510].structtype[single].field[rankfeatures].id[0]
+datatype[-205818510].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[-205818510].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[-205818510].structtype[single].field[summaryfeatures].id[0]
+datatype[-205818510].structtype[single].inherits[0]
+datatype[-205818510].weightedsettype[0]
+datatype[-384824039].id -384824039
+datatype[-384824039].annotationreftype[0]
+datatype[-384824039].arraytype[0]
+datatype[-384824039].documenttype[0]
+datatype[-384824039].structtype[1]
+datatype[-384824039].structtype[single].name "mother_search.header"
+datatype[-384824039].structtype[single].version 0
+datatype[-384824039].structtype[single].field[5]
+datatype[-384824039].structtype[single].field[onlygrandparent].datatype 0
+datatype[-384824039].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[-384824039].structtype[single].field[onlygrandparent].id[0]
+datatype[-384824039].structtype[single].field[onlymother].datatype 2
+datatype[-384824039].structtype[single].field[onlymother].name "onlymother"
+datatype[-384824039].structtype[single].field[onlymother].id[0]
+datatype[-384824039].structtype[single].field[overridden].datatype 0
+datatype[-384824039].structtype[single].field[overridden].name "overridden"
+datatype[-384824039].structtype[single].field[overridden].id[0]
+datatype[-384824039].structtype[single].field[rankfeatures].datatype 147991900
+datatype[-384824039].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[-384824039].structtype[single].field[rankfeatures].id[0]
+datatype[-384824039].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[-384824039].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[-384824039].structtype[single].field[summaryfeatures].id[0]
+datatype[-384824039].structtype[single].inherits[0]
+datatype[-384824039].weightedsettype[0]
+datatype[-52742073].id -52742073
+datatype[-52742073].annotationreftype[0]
+datatype[-52742073].arraytype[0]
+datatype[-52742073].documenttype[0]
+datatype[-52742073].structtype[1]
+datatype[-52742073].structtype[single].name "father_search.body"
+datatype[-52742073].structtype[single].version 0
+datatype[-52742073].structtype[single].field[0]
+datatype[-52742073].structtype[single].inherits[0]
+datatype[-52742073].weightedsettype[0]
+datatype[-580592339].id -580592339
+datatype[-580592339].annotationreftype[0]
+datatype[-580592339].arraytype[0]
+datatype[-580592339].documenttype[1]
+datatype[-580592339].documenttype[single].bodystruct -1467672569
+datatype[-580592339].documenttype[single].headerstruct -205818510
+datatype[-580592339].documenttype[single].name "child_search"
+datatype[-580592339].documenttype[single].version 0
+datatype[-580592339].documenttype[single].inherits[0]
+datatype[-580592339].structtype[0]
+datatype[-580592339].weightedsettype[0]
+datatype[-876064862].id -876064862
+datatype[-876064862].annotationreftype[0]
+datatype[-876064862].arraytype[0]
+datatype[-876064862].documenttype[0]
+datatype[-876064862].structtype[1]
+datatype[-876064862].structtype[single].name "search_position"
+datatype[-876064862].structtype[single].version 0
+datatype[-876064862].structtype[single].field[2]
+datatype[-876064862].structtype[single].field[x].datatype 0
+datatype[-876064862].structtype[single].field[x].name "x"
+datatype[-876064862].structtype[single].field[x].id[0]
+datatype[-876064862].structtype[single].field[y].datatype 0
+datatype[-876064862].structtype[single].field[y].name "y"
+datatype[-876064862].structtype[single].field[y].id[0]
+datatype[-876064862].structtype[single].inherits[0]
+datatype[-876064862].weightedsettype[0]
+datatype[1306663898].id 1306663898
+datatype[1306663898].annotationreftype[0]
+datatype[1306663898].arraytype[0]
+datatype[1306663898].documenttype[0]
+datatype[1306663898].structtype[1]
+datatype[1306663898].structtype[single].name "mother.header"
+datatype[1306663898].structtype[single].version 0
+datatype[1306663898].structtype[single].field[1]
+datatype[1306663898].structtype[single].field[onlymother].datatype 2
+datatype[1306663898].structtype[single].field[onlymother].name "onlymother"
+datatype[1306663898].structtype[single].field[onlymother].id[0]
+datatype[1306663898].structtype[single].inherits[0]
+datatype[1306663898].weightedsettype[0]
+datatype[1464571117].id 1464571117
+datatype[1464571117].annotationreftype[0]
+datatype[1464571117].arraytype[0]
+datatype[1464571117].documenttype[1]
+datatype[1464571117].documenttype[single].bodystruct -52742073
+datatype[1464571117].documenttype[single].headerstruct -1962244686
+datatype[1464571117].documenttype[single].name "father_search"
+datatype[1464571117].documenttype[single].version 0
+datatype[1464571117].documenttype[single].inherits[0]
+datatype[1464571117].structtype[0]
+datatype[1464571117].weightedsettype[0]
+datatype[147991900].id 147991900
+datatype[147991900].annotationreftype[0]
+datatype[147991900].arraytype[1]
+datatype[147991900].arraytype[single].datatype -1740240543
+datatype[147991900].documenttype[0]
+datatype[147991900].structtype[0]
+datatype[147991900].weightedsettype[0]
+datatype[1530060044].id 1530060044
+datatype[1530060044].annotationreftype[0]
+datatype[1530060044].arraytype[0]
+datatype[1530060044].documenttype[0]
+datatype[1530060044].structtype[1]
+datatype[1530060044].structtype[single].name "grandparent_search.header"
+datatype[1530060044].structtype[single].version 0
+datatype[1530060044].structtype[single].field[4]
+datatype[1530060044].structtype[single].field[onlygrandparent].datatype 0
+datatype[1530060044].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[1530060044].structtype[single].field[onlygrandparent].id[0]
+datatype[1530060044].structtype[single].field[overridden].datatype 0
+datatype[1530060044].structtype[single].field[overridden].name "overridden"
+datatype[1530060044].structtype[single].field[overridden].id[0]
+datatype[1530060044].structtype[single].field[rankfeatures].datatype 147991900
+datatype[1530060044].structtype[single].field[rankfeatures].name "rankfeatures"
+datatype[1530060044].structtype[single].field[rankfeatures].id[0]
+datatype[1530060044].structtype[single].field[summaryfeatures].datatype 147991900
+datatype[1530060044].structtype[single].field[summaryfeatures].name "summaryfeatures"
+datatype[1530060044].structtype[single].field[summaryfeatures].id[0]
+datatype[1530060044].structtype[single].inherits[0]
+datatype[1530060044].weightedsettype[0]
+datatype[1845861921].id 1845861921
+datatype[1845861921].annotationreftype[0]
+datatype[1845861921].arraytype[0]
+datatype[1845861921].documenttype[0]
+datatype[1845861921].structtype[1]
+datatype[1845861921].structtype[single].name "grandparent_search.body"
+datatype[1845861921].structtype[single].version 0
+datatype[1845861921].structtype[single].field[0]
+datatype[1845861921].structtype[single].inherits[0]
+datatype[1845861921].weightedsettype[0]
+datatype[2126589281].id 2126589281
+datatype[2126589281].annotationreftype[0]
+datatype[2126589281].arraytype[0]
+datatype[2126589281].documenttype[0]
+datatype[2126589281].structtype[1]
+datatype[2126589281].structtype[single].name "father.header"
+datatype[2126589281].structtype[single].version 0
+datatype[2126589281].structtype[single].field[1]
+datatype[2126589281].structtype[single].field[onlyfather].datatype 2
+datatype[2126589281].structtype[single].field[onlyfather].name "onlyfather"
+datatype[2126589281].structtype[single].field[onlyfather].id[0]
+datatype[2126589281].structtype[single].inherits[0]
+datatype[2126589281].weightedsettype[0]
+datatype[328953555].id 328953555
+datatype[328953555].annotationreftype[0]
+datatype[328953555].arraytype[0]
+datatype[328953555].documenttype[1]
+datatype[328953555].documenttype[single].bodystruct 1845861921
+datatype[328953555].documenttype[single].headerstruct 1530060044
+datatype[328953555].documenttype[single].name "grandparent_search"
+datatype[328953555].documenttype[single].version 0
+datatype[328953555].documenttype[single].inherits[0]
+datatype[328953555].structtype[0]
+datatype[328953555].weightedsettype[0]
+datatype[464784087].id 464784087
+datatype[464784087].annotationreftype[0]
+datatype[464784087].arraytype[0]
+datatype[464784087].documenttype[0]
+datatype[464784087].structtype[1]
+datatype[464784087].structtype[single].name "search_uri"
+datatype[464784087].structtype[single].version 0
+datatype[464784087].structtype[single].field[7]
+datatype[464784087].structtype[single].field[all].datatype 2
+datatype[464784087].structtype[single].field[all].name "all"
+datatype[464784087].structtype[single].field[all].id[0]
+datatype[464784087].structtype[single].field[fragment].datatype 2
+datatype[464784087].structtype[single].field[fragment].name "fragment"
+datatype[464784087].structtype[single].field[fragment].id[0]
+datatype[464784087].structtype[single].field[host].datatype 2
+datatype[464784087].structtype[single].field[host].name "host"
+datatype[464784087].structtype[single].field[host].id[0]
+datatype[464784087].structtype[single].field[path].datatype 2
+datatype[464784087].structtype[single].field[path].name "path"
+datatype[464784087].structtype[single].field[path].id[0]
+datatype[464784087].structtype[single].field[port].datatype 0
+datatype[464784087].structtype[single].field[port].name "port"
+datatype[464784087].structtype[single].field[port].id[0]
+datatype[464784087].structtype[single].field[query].datatype 2
+datatype[464784087].structtype[single].field[query].name "query"
+datatype[464784087].structtype[single].field[query].id[0]
+datatype[464784087].structtype[single].field[scheme].datatype 2
+datatype[464784087].structtype[single].field[scheme].name "scheme"
+datatype[464784087].structtype[single].field[scheme].id[0]
+datatype[464784087].structtype[single].inherits[0]
+datatype[464784087].weightedsettype[0]
+datatype[644645734].id 644645734
+datatype[644645734].annotationreftype[0]
+datatype[644645734].arraytype[0]
+datatype[644645734].documenttype[1]
+datatype[644645734].documenttype[single].bodystruct -1852215954
+datatype[644645734].documenttype[single].headerstruct -384824039
+datatype[644645734].documenttype[single].name "mother_search"
+datatype[644645734].documenttype[single].version 0
+datatype[644645734].documenttype[single].inherits[0]
+datatype[644645734].structtype[0]
+datatype[644645734].weightedsettype[0]
+datatype[746267614].id 746267614
+datatype[746267614].annotationreftype[0]
+datatype[746267614].arraytype[0]
+datatype[746267614].documenttype[1]
+datatype[746267614].documenttype[single].bodystruct -126593034
+datatype[746267614].documenttype[single].headerstruct 81425825
+datatype[746267614].documenttype[single].name "child"
+datatype[746267614].documenttype[single].version 0
+datatype[746267614].documenttype[single].inherits[2]
+datatype[746267614].documenttype[single].inherits[father].name "father"
+datatype[746267614].documenttype[single].inherits[father].version 0
+datatype[746267614].documenttype[single].inherits[mother].name "mother"
+datatype[746267614].documenttype[single].inherits[mother].version 0
+datatype[746267614].structtype[0]
+datatype[746267614].weightedsettype[0]
+datatype[81425825].id 81425825
+datatype[81425825].annotationreftype[0]
+datatype[81425825].arraytype[0]
+datatype[81425825].documenttype[0]
+datatype[81425825].structtype[1]
+datatype[81425825].structtype[single].name "child.header"
+datatype[81425825].structtype[single].version 0
+datatype[81425825].structtype[single].field[1]
+datatype[81425825].structtype[single].field[onlychild].datatype 2
+datatype[81425825].structtype[single].field[onlychild].name "onlychild"
+datatype[81425825].structtype[single].field[onlychild].id[0]
+datatype[81425825].structtype[single].inherits[0]
+datatype[81425825].weightedsettype[0]
+datatype[978262812].id 978262812
+datatype[978262812].annotationreftype[0]
+datatype[978262812].arraytype[0]
+datatype[978262812].documenttype[0]
+datatype[978262812].structtype[1]
+datatype[978262812].structtype[single].name "grandparent.body"
+datatype[978262812].structtype[single].version 0
+datatype[978262812].structtype[single].field[0]
+datatype[978262812].structtype[single].inherits[0]
+datatype[978262812].weightedsettype[0]
+datatype[986686494].id 986686494
+datatype[986686494].annotationreftype[0]
+datatype[986686494].arraytype[0]
+datatype[986686494].documenttype[1]
+datatype[986686494].documenttype[single].bodystruct -1742340170
+datatype[986686494].documenttype[single].headerstruct 2126589281
+datatype[986686494].documenttype[single].name "father"
+datatype[986686494].documenttype[single].version 0
+datatype[986686494].documenttype[single].inherits[1]
+datatype[986686494].documenttype[single].inherits[grandparent].name "grandparent"
+datatype[986686494].documenttype[single].inherits[grandparent].version 0
+datatype[986686494].structtype[0]
+datatype[986686494].weightedsettype[0]
+datatype[990971719].id 990971719
+datatype[990971719].annotationreftype[0]
+datatype[990971719].arraytype[0]
+datatype[990971719].documenttype[0]
+datatype[990971719].structtype[1]
+datatype[990971719].structtype[single].name "grandparent.header"
+datatype[990971719].structtype[single].version 0
+datatype[990971719].structtype[single].field[2]
+datatype[990971719].structtype[single].field[onlygrandparent].datatype 0
+datatype[990971719].structtype[single].field[onlygrandparent].name "onlygrandparent"
+datatype[990971719].structtype[single].field[onlygrandparent].id[0]
+datatype[990971719].structtype[single].field[overridden].datatype 0
+datatype[990971719].structtype[single].field[overridden].name "overridden"
+datatype[990971719].structtype[single].field[overridden].id[0]
+datatype[990971719].structtype[single].inherits[0]
+datatype[990971719].weightedsettype[0]
diff --git a/config-model/src/test/derived/inheritance/rank-profiles.cfg b/config-model/src/test/derived/inheritance/rank-profiles.cfg
new file mode 100644
index 00000000000..abfea4714d5
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/rank-profiles.cfg
@@ -0,0 +1,16 @@
+rankprofile[2]
+rankprofile[default].name "default"
+rankprofile[default].fef.property[0]
+rankprofile[unranked].binhigh 0
+rankprofile[unranked].binlow 0
+rankprofile[unranked].binsize 0.0
+rankprofile[unranked].name "unranked"
+rankprofile[unranked].fef.property[4]
+rankprofile[unranked].fef.property[a00000].name "vespa.rank.firstphase"
+rankprofile[unranked].fef.property[a00000].value "value(0)"
+rankprofile[unranked].fef.property[a00001].name "vespa.hitcollector.heapsize"
+rankprofile[unranked].fef.property[a00001].value "0"
+rankprofile[unranked].fef.property[a00002].name "vespa.hitcollector.arraysize"
+rankprofile[unranked].fef.property[a00002].value "0"
+rankprofile[unranked].fef.property[a00003].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[unranked].fef.property[a00003].value "true"
diff --git a/config-model/src/test/derived/inheritance/summary.cfg b/config-model/src/test/derived/inheritance/summary.cfg
new file mode 100644
index 00000000000..c396dc780b4
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/summary.cfg
@@ -0,0 +1,26 @@
+defaultsummaryid 1570256615
+classes[2]
+classes[1570256615].id 1570256615
+classes[1570256615].name "child"
+classes[1570256615].fields[4]
+classes[1570256615].fields[documentid].name "documentid"
+classes[1570256615].fields[documentid].type "longstring"
+classes[1570256615].fields[onlyfather].name "onlyfather"
+classes[1570256615].fields[onlyfather].type "longstring"
+classes[1570256615].fields[rankfeatures].name "rankfeatures"
+classes[1570256615].fields[rankfeatures].type "longstring"
+classes[1570256615].fields[summaryfeatures].name "summaryfeatures"
+classes[1570256615].fields[summaryfeatures].type "longstring"
+classes[306313061].id 306313061
+classes[306313061].name "attributeprefetch"
+classes[306313061].fields[5]
+classes[306313061].fields[onlygrandparent].name "onlygrandparent"
+classes[306313061].fields[onlygrandparent].type "integer"
+classes[306313061].fields[onlymother].name "onlymother"
+classes[306313061].fields[onlymother].type "longstring"
+classes[306313061].fields[overridden].name "overridden"
+classes[306313061].fields[overridden].type "integer"
+classes[306313061].fields[rankfeatures].name "rankfeatures"
+classes[306313061].fields[rankfeatures].type "longstring"
+classes[306313061].fields[summaryfeatures].name "summaryfeatures"
+classes[306313061].fields[summaryfeatures].type "longstring"
diff --git a/config-model/src/test/derived/inheritance/summarymap.cfg b/config-model/src/test/derived/inheritance/summarymap.cfg
new file mode 100644
index 00000000000..c92c8e1cf6c
--- /dev/null
+++ b/config-model/src/test/derived/inheritance/summarymap.cfg
@@ -0,0 +1,17 @@
+defaultoutputclass -1
+override[5]
+override[onlygrandparent].arguments "onlygrandparent"
+override[onlygrandparent].command "attribute"
+override[onlygrandparent].field "onlygrandparent"
+override[onlymother].arguments "onlymother"
+override[onlymother].command "attribute"
+override[onlymother].field "onlymother"
+override[overridden].arguments "overridden"
+override[overridden].command "attribute"
+override[overridden].field "overridden"
+override[rankfeatures].arguments ""
+override[rankfeatures].command "rankfeatures"
+override[rankfeatures].field "rankfeatures"
+override[summaryfeatures].arguments ""
+override[summaryfeatures].command "summaryfeatures"
+override[summaryfeatures].field "summaryfeatures"
diff --git a/config-model/src/test/derived/inheritancebadtypes/child.sd b/config-model/src/test/derived/inheritancebadtypes/child.sd
new file mode 100644
index 00000000000..5a3f5b46b24
--- /dev/null
+++ b/config-model/src/test/derived/inheritancebadtypes/child.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+document child inherits parent {
+ field a type int {
+ indexing: index
+ }
+}
+}
diff --git a/config-model/src/test/derived/inheritancebadtypes/parent.sd b/config-model/src/test/derived/inheritancebadtypes/parent.sd
new file mode 100644
index 00000000000..470faf4fec6
--- /dev/null
+++ b/config-model/src/test/derived/inheritancebadtypes/parent.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search parent {
+document parent {
+ field a type string {
+ indexing: index
+ }
+}
+}
diff --git a/config-model/src/test/derived/inheritdiamond/.gitignore b/config-model/src/test/derived/inheritdiamond/.gitignore
new file mode 100644
index 00000000000..fc6b63d95e9
--- /dev/null
+++ b/config-model/src/test/derived/inheritdiamond/.gitignore
@@ -0,0 +1 @@
+/documentmanager.15.cfg
diff --git a/config-model/src/test/derived/inheritdiamond/child.sd b/config-model/src/test/derived/inheritdiamond/child.sd
new file mode 100644
index 00000000000..795f01edbaa
--- /dev/null
+++ b/config-model/src/test/derived/inheritdiamond/child.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+ document child inherits mother, father {
+ struct child_struct {
+ field child_field type string { }
+ }
+ field foo type grandparent_struct { }
+ field bar type mother_struct { }
+ field baz type father_struct { }
+ field cox type child_struct { }
+ }
+}
diff --git a/config-model/src/test/derived/inheritdiamond/documentmanager.cfg b/config-model/src/test/derived/inheritdiamond/documentmanager.cfg
new file mode 100644
index 00000000000..5f3edfb8475
--- /dev/null
+++ b/config-model/src/test/derived/inheritdiamond/documentmanager.cfg
@@ -0,0 +1,269 @@
+enablecompression false
+datatype[0].id -126593034
+datatype[0].structtype[0].name "child.body"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[1].id 336538650
+datatype[1].structtype[0].name "child_struct"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "child_field"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id 81425825
+datatype[2].structtype[0].name "child.header"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "foo"
+datatype[2].structtype[0].field[0].datatype 1246084544
+datatype[2].structtype[0].field[1].name "bar"
+datatype[2].structtype[0].field[1].datatype 1561776723
+datatype[2].structtype[0].field[2].name "baz"
+datatype[2].structtype[0].field[2].datatype -1913265190
+datatype[2].structtype[0].field[3].name "cox"
+datatype[2].structtype[0].field[3].datatype 336538650
+datatype[3].id 746267614
+datatype[3].documenttype[0].name "child"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "mother"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].inherits[1].name "document"
+datatype[3].documenttype[0].inherits[1].version 0
+datatype[3].documenttype[0].inherits[2].name "father"
+datatype[3].documenttype[0].inherits[2].version 0
+datatype[3].documenttype[0].headerstruct 81425825
+datatype[3].documenttype[0].bodystruct -126593034
+datatype[4].id -1913265190
+datatype[4].structtype[0].name "father_struct"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[4].structtype[0].field[0].name "father_field"
+datatype[4].structtype[0].field[0].datatype 2
+datatype[5].id 1246084544
+datatype[5].structtype[0].name "grandparent_struct"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "grandparent_field"
+datatype[5].structtype[0].field[0].datatype 2
+datatype[6].id -1962244686
+datatype[6].structtype[0].name "father_search.header"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[6].structtype[0].field[0].name "rankfeatures"
+datatype[6].structtype[0].field[0].datatype 2
+datatype[6].structtype[0].field[1].name "summaryfeatures"
+datatype[6].structtype[0].field[1].datatype 2
+datatype[7].id -52742073
+datatype[7].structtype[0].name "father_search.body"
+datatype[7].structtype[0].version 0
+datatype[7].structtype[0].compresstype NONE
+datatype[7].structtype[0].compresslevel 0
+datatype[7].structtype[0].compressthreshold 95
+datatype[7].structtype[0].compressminsize 800
+datatype[8].id 1464571117
+datatype[8].documenttype[0].name "father_search"
+datatype[8].documenttype[0].version 0
+datatype[8].documenttype[0].inherits[0].name "document"
+datatype[8].documenttype[0].inherits[0].version 0
+datatype[8].documenttype[0].headerstruct -1962244686
+datatype[8].documenttype[0].bodystruct -52742073
+datatype[9].id -1852215954
+datatype[9].structtype[0].name "mother_search.body"
+datatype[9].structtype[0].version 0
+datatype[9].structtype[0].compresstype NONE
+datatype[9].structtype[0].compresslevel 0
+datatype[9].structtype[0].compressthreshold 95
+datatype[9].structtype[0].compressminsize 800
+datatype[10].id -384824039
+datatype[10].structtype[0].name "mother_search.header"
+datatype[10].structtype[0].version 0
+datatype[10].structtype[0].compresstype NONE
+datatype[10].structtype[0].compresslevel 0
+datatype[10].structtype[0].compressthreshold 95
+datatype[10].structtype[0].compressminsize 800
+datatype[10].structtype[0].field[0].name "rankfeatures"
+datatype[10].structtype[0].field[0].datatype 2
+datatype[10].structtype[0].field[1].name "summaryfeatures"
+datatype[10].structtype[0].field[1].datatype 2
+datatype[11].id 1561776723
+datatype[11].structtype[0].name "mother_struct"
+datatype[11].structtype[0].version 0
+datatype[11].structtype[0].compresstype NONE
+datatype[11].structtype[0].compresslevel 0
+datatype[11].structtype[0].compressthreshold 95
+datatype[11].structtype[0].compressminsize 800
+datatype[11].structtype[0].field[0].name "mother_field"
+datatype[11].structtype[0].field[0].datatype 2
+datatype[12].id 644645734
+datatype[12].documenttype[0].name "mother_search"
+datatype[12].documenttype[0].version 0
+datatype[12].documenttype[0].inherits[0].name "document"
+datatype[12].documenttype[0].inherits[0].version 0
+datatype[12].documenttype[0].headerstruct -384824039
+datatype[12].documenttype[0].bodystruct -1852215954
+datatype[13].id 1306663898
+datatype[13].structtype[0].name "mother.header"
+datatype[13].structtype[0].version 0
+datatype[13].structtype[0].compresstype NONE
+datatype[13].structtype[0].compresslevel 0
+datatype[13].structtype[0].compressthreshold 95
+datatype[13].structtype[0].compressminsize 800
+datatype[14].id -1989003153
+datatype[14].structtype[0].name "mother.body"
+datatype[14].structtype[0].version 0
+datatype[14].structtype[0].compresstype NONE
+datatype[14].structtype[0].compresslevel 0
+datatype[14].structtype[0].compressthreshold 95
+datatype[14].structtype[0].compressminsize 800
+datatype[15].id -158393403
+datatype[15].documenttype[0].name "mother"
+datatype[15].documenttype[0].version 0
+datatype[15].documenttype[0].inherits[0].name "grandparent"
+datatype[15].documenttype[0].inherits[0].version 0
+datatype[15].documenttype[0].inherits[1].name "document"
+datatype[15].documenttype[0].inherits[1].version 0
+datatype[15].documenttype[0].headerstruct 1306663898
+datatype[15].documenttype[0].bodystruct -1989003153
+datatype[16].id -205818510
+datatype[16].structtype[0].name "child_search.header"
+datatype[16].structtype[0].version 0
+datatype[16].structtype[0].compresstype NONE
+datatype[16].structtype[0].compresslevel 0
+datatype[16].structtype[0].compressthreshold 95
+datatype[16].structtype[0].compressminsize 800
+datatype[16].structtype[0].field[0].name "rankfeatures"
+datatype[16].structtype[0].field[0].datatype 2
+datatype[16].structtype[0].field[1].name "summaryfeatures"
+datatype[16].structtype[0].field[1].datatype 2
+datatype[17].id -1467672569
+datatype[17].structtype[0].name "child_search.body"
+datatype[17].structtype[0].version 0
+datatype[17].structtype[0].compresstype NONE
+datatype[17].structtype[0].compresslevel 0
+datatype[17].structtype[0].compressthreshold 95
+datatype[17].structtype[0].compressminsize 800
+datatype[18].id -580592339
+datatype[18].documenttype[0].name "child_search"
+datatype[18].documenttype[0].version 0
+datatype[18].documenttype[0].inherits[0].name "document"
+datatype[18].documenttype[0].inherits[0].version 0
+datatype[18].documenttype[0].headerstruct -205818510
+datatype[18].documenttype[0].bodystruct -1467672569
+datatype[19].id 111553393
+datatype[19].structtype[0].name "url"
+datatype[19].structtype[0].version 0
+datatype[19].structtype[0].compresstype NONE
+datatype[19].structtype[0].compresslevel 0
+datatype[19].structtype[0].compressthreshold 95
+datatype[19].structtype[0].compressminsize 800
+datatype[19].structtype[0].field[0].name "all"
+datatype[19].structtype[0].field[0].datatype 2
+datatype[19].structtype[0].field[1].name "scheme"
+datatype[19].structtype[0].field[1].datatype 2
+datatype[19].structtype[0].field[2].name "host"
+datatype[19].structtype[0].field[2].datatype 2
+datatype[19].structtype[0].field[3].name "port"
+datatype[19].structtype[0].field[3].datatype 2
+datatype[19].structtype[0].field[4].name "path"
+datatype[19].structtype[0].field[4].datatype 2
+datatype[19].structtype[0].field[5].name "query"
+datatype[19].structtype[0].field[5].datatype 2
+datatype[19].structtype[0].field[6].name "fragment"
+datatype[19].structtype[0].field[6].datatype 2
+datatype[20].id 1381038251
+datatype[20].structtype[0].name "position"
+datatype[20].structtype[0].version 0
+datatype[20].structtype[0].compresstype NONE
+datatype[20].structtype[0].compresslevel 0
+datatype[20].structtype[0].compressthreshold 95
+datatype[20].structtype[0].compressminsize 800
+datatype[20].structtype[0].field[0].name "x"
+datatype[20].structtype[0].field[0].datatype 0
+datatype[20].structtype[0].field[1].name "y"
+datatype[20].structtype[0].field[1].datatype 0
+datatype[21].id 1845861921
+datatype[21].structtype[0].name "grandparent_search.body"
+datatype[21].structtype[0].version 0
+datatype[21].structtype[0].compresstype NONE
+datatype[21].structtype[0].compresslevel 0
+datatype[21].structtype[0].compressthreshold 95
+datatype[21].structtype[0].compressminsize 800
+datatype[22].id 1530060044
+datatype[22].structtype[0].name "grandparent_search.header"
+datatype[22].structtype[0].version 0
+datatype[22].structtype[0].compresstype NONE
+datatype[22].structtype[0].compresslevel 0
+datatype[22].structtype[0].compressthreshold 95
+datatype[22].structtype[0].compressminsize 800
+datatype[22].structtype[0].field[0].name "rankfeatures"
+datatype[22].structtype[0].field[0].datatype 2
+datatype[22].structtype[0].field[1].name "summaryfeatures"
+datatype[22].structtype[0].field[1].datatype 2
+datatype[23].id 328953555
+datatype[23].documenttype[0].name "grandparent_search"
+datatype[23].documenttype[0].version 0
+datatype[23].documenttype[0].inherits[0].name "document"
+datatype[23].documenttype[0].inherits[0].version 0
+datatype[23].documenttype[0].headerstruct 1530060044
+datatype[23].documenttype[0].bodystruct 1845861921
+datatype[24].id 990971719
+datatype[24].structtype[0].name "grandparent.header"
+datatype[24].structtype[0].version 0
+datatype[24].structtype[0].compresstype NONE
+datatype[24].structtype[0].compresslevel 0
+datatype[24].structtype[0].compressthreshold 95
+datatype[24].structtype[0].compressminsize 800
+datatype[25].id 978262812
+datatype[25].structtype[0].name "grandparent.body"
+datatype[25].structtype[0].version 0
+datatype[25].structtype[0].compresstype NONE
+datatype[25].structtype[0].compresslevel 0
+datatype[25].structtype[0].compressthreshold 95
+datatype[25].structtype[0].compressminsize 800
+datatype[26].id -154107656
+datatype[26].documenttype[0].name "grandparent"
+datatype[26].documenttype[0].version 0
+datatype[26].documenttype[0].inherits[0].name "document"
+datatype[26].documenttype[0].inherits[0].version 0
+datatype[26].documenttype[0].headerstruct 990971719
+datatype[26].documenttype[0].bodystruct 978262812
+datatype[27].id -1742340170
+datatype[27].structtype[0].name "father.body"
+datatype[27].structtype[0].version 0
+datatype[27].structtype[0].compresstype NONE
+datatype[27].structtype[0].compresslevel 0
+datatype[27].structtype[0].compressthreshold 95
+datatype[27].structtype[0].compressminsize 800
+datatype[28].id 2126589281
+datatype[28].structtype[0].name "father.header"
+datatype[28].structtype[0].version 0
+datatype[28].structtype[0].compresstype NONE
+datatype[28].structtype[0].compresslevel 0
+datatype[28].structtype[0].compressthreshold 95
+datatype[28].structtype[0].compressminsize 800
+datatype[29].id 986686494
+datatype[29].documenttype[0].name "father"
+datatype[29].documenttype[0].version 0
+datatype[29].documenttype[0].inherits[0].name "grandparent"
+datatype[29].documenttype[0].inherits[0].version 0
+datatype[29].documenttype[0].inherits[1].name "document"
+datatype[29].documenttype[0].inherits[1].version 0
+datatype[29].documenttype[0].headerstruct 2126589281
+datatype[29].documenttype[0].bodystruct -1742340170
diff --git a/config-model/src/test/derived/inheritdiamond/father.sd b/config-model/src/test/derived/inheritdiamond/father.sd
new file mode 100644
index 00000000000..59bd145cf5a
--- /dev/null
+++ b/config-model/src/test/derived/inheritdiamond/father.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search father {
+ document father inherits grandparent {
+ struct father_struct {
+ field father_field type string { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/inheritdiamond/grandparent.sd b/config-model/src/test/derived/inheritdiamond/grandparent.sd
new file mode 100644
index 00000000000..2944c0f32cc
--- /dev/null
+++ b/config-model/src/test/derived/inheritdiamond/grandparent.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search grandparent {
+ document grandparent {
+ struct grandparent_struct {
+ field grandparent_field type string { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/inheritdiamond/mother.sd b/config-model/src/test/derived/inheritdiamond/mother.sd
new file mode 100644
index 00000000000..cd5fa8cae4d
--- /dev/null
+++ b/config-model/src/test/derived/inheritdiamond/mother.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search mother {
+ document mother inherits grandparent {
+ struct mother_struct {
+ field mother_field type string { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/inheritfromgrandparent/child.sd b/config-model/src/test/derived/inheritfromgrandparent/child.sd
new file mode 100644
index 00000000000..891a92323b1
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromgrandparent/child.sd
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+ document child inherits parent {
+ field child_field type grandparent_struct { }
+ }
+}
diff --git a/config-model/src/test/derived/inheritfromgrandparent/documentmanager.cfg b/config-model/src/test/derived/inheritfromgrandparent/documentmanager.cfg
new file mode 100644
index 00000000000..8c51dc52c58
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromgrandparent/documentmanager.cfg
@@ -0,0 +1,99 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 1246084544
+datatype[1].structtype[0].name "grandparent_struct"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "grandparent_field"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id 990971719
+datatype[2].structtype[0].name "grandparent.header"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "rankfeatures"
+datatype[2].structtype[0].field[0].datatype 2
+datatype[2].structtype[0].field[1].name "summaryfeatures"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[3].id 978262812
+datatype[3].structtype[0].name "grandparent.body"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[4].id -154107656
+datatype[4].documenttype[0].name "grandparent"
+datatype[4].documenttype[0].version 0
+datatype[4].documenttype[0].inherits[0].name "document"
+datatype[4].documenttype[0].inherits[0].version 0
+datatype[4].documenttype[0].headerstruct 990971719
+datatype[4].documenttype[0].bodystruct 978262812
+datatype[5].id 836075987
+datatype[5].structtype[0].name "parent.header"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[6].id -389494616
+datatype[6].structtype[0].name "parent.body"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[7].id 1175161836
+datatype[7].documenttype[0].name "parent"
+datatype[7].documenttype[0].version 0
+datatype[7].documenttype[0].inherits[0].name "grandparent"
+datatype[7].documenttype[0].inherits[0].version 0
+datatype[7].documenttype[0].inherits[1].name "document"
+datatype[7].documenttype[0].inherits[1].version 0
+datatype[7].documenttype[0].headerstruct 836075987
+datatype[7].documenttype[0].bodystruct -389494616
+datatype[7].documenttype[0].fieldsets{[document]}.fields[0] "rankfeatures"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[1] "summaryfeatures"
+datatype[8].id 81425825
+datatype[8].structtype[0].name "child.header"
+datatype[8].structtype[0].version 0
+datatype[8].structtype[0].compresstype NONE
+datatype[8].structtype[0].compresslevel 0
+datatype[8].structtype[0].compressthreshold 95
+datatype[8].structtype[0].compressminsize 800
+datatype[8].structtype[0].field[0].name "child_field"
+datatype[8].structtype[0].field[0].datatype 1246084544
+datatype[9].id -126593034
+datatype[9].structtype[0].name "child.body"
+datatype[9].structtype[0].version 0
+datatype[9].structtype[0].compresstype NONE
+datatype[9].structtype[0].compresslevel 0
+datatype[9].structtype[0].compressthreshold 95
+datatype[9].structtype[0].compressminsize 800
+datatype[10].id 746267614
+datatype[10].documenttype[0].name "child"
+datatype[10].documenttype[0].version 0
+datatype[10].documenttype[0].inherits[0].name "document"
+datatype[10].documenttype[0].inherits[0].version 0
+datatype[10].documenttype[0].inherits[1].name "parent"
+datatype[10].documenttype[0].inherits[1].version 0
+datatype[10].documenttype[0].headerstruct 81425825
+datatype[10].documenttype[0].bodystruct -126593034
+datatype[10].documenttype[0].fieldsets{[document]}.fields[0] "child_field"
+datatype[10].documenttype[0].fieldsets{[document]}.fields[1] "rankfeatures"
+datatype[10].documenttype[0].fieldsets{[document]}.fields[2] "summaryfeatures"
diff --git a/config-model/src/test/derived/inheritfromgrandparent/grandparent.sd b/config-model/src/test/derived/inheritfromgrandparent/grandparent.sd
new file mode 100644
index 00000000000..2944c0f32cc
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromgrandparent/grandparent.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search grandparent {
+ document grandparent {
+ struct grandparent_struct {
+ field grandparent_field type string { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/inheritfromgrandparent/parent.sd b/config-model/src/test/derived/inheritfromgrandparent/parent.sd
new file mode 100644
index 00000000000..626d0c95657
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromgrandparent/parent.sd
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search parent {
+ document parent inherits grandparent {
+
+ }
+}
diff --git a/config-model/src/test/derived/inheritfromnull/inheritfromnull.sd b/config-model/src/test/derived/inheritfromnull/inheritfromnull.sd
new file mode 100644
index 00000000000..bac40a07284
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromnull/inheritfromnull.sd
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search inheritfromnull {
+ document inheritfromnull inherits foo {
+ }
+}
diff --git a/config-model/src/test/derived/inheritfromparent/attributes.cfg b/config-model/src/test/derived/inheritfromparent/attributes.cfg
new file mode 100644
index 00000000000..a5e912fb984
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromparent/attributes.cfg
@@ -0,0 +1,19 @@
+attribute[0].name "weight"
+attribute[0].datatype FLOAT
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/inheritfromparent/child.sd b/config-model/src/test/derived/inheritfromparent/child.sd
new file mode 100644
index 00000000000..5c0242c9acc
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromparent/child.sd
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+ document child inherits parent {
+ field child_field type parent_struct { }
+ }
+}
diff --git a/config-model/src/test/derived/inheritfromparent/documentmanager.cfg b/config-model/src/test/derived/inheritfromparent/documentmanager.cfg
new file mode 100644
index 00000000000..fec59f5ff72
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromparent/documentmanager.cfg
@@ -0,0 +1,81 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 1091188812
+datatype[1].structtype[0].name "parent_struct"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "parent_field"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[2].id 836075987
+datatype[2].structtype[0].name "parent.header"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "weight_src"
+datatype[2].structtype[0].field[0].datatype 1
+datatype[2].structtype[0].field[1].name "weight"
+datatype[2].structtype[0].field[1].datatype 1
+datatype[2].structtype[0].field[2].name "rankfeatures"
+datatype[2].structtype[0].field[2].datatype 2
+datatype[2].structtype[0].field[3].name "summaryfeatures"
+datatype[2].structtype[0].field[3].datatype 2
+datatype[3].id -389494616
+datatype[3].structtype[0].name "parent.body"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[4].id 1175161836
+datatype[4].documenttype[0].name "parent"
+datatype[4].documenttype[0].version 0
+datatype[4].documenttype[0].inherits[0].name "document"
+datatype[4].documenttype[0].inherits[0].version 0
+datatype[4].documenttype[0].headerstruct 836075987
+datatype[4].documenttype[0].bodystruct -389494616
+datatype[4].documenttype[0].fieldsets{[document]}.fields[0] "weight_src"
+datatype[5].id 81425825
+datatype[5].structtype[0].name "child.header"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "child_field"
+datatype[5].structtype[0].field[0].datatype 1091188812
+datatype[6].id -126593034
+datatype[6].structtype[0].name "child.body"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[7].id 746267614
+datatype[7].documenttype[0].name "child"
+datatype[7].documenttype[0].version 0
+datatype[7].documenttype[0].inherits[0].name "document"
+datatype[7].documenttype[0].inherits[0].version 0
+datatype[7].documenttype[0].inherits[1].name "parent"
+datatype[7].documenttype[0].inherits[1].version 0
+datatype[7].documenttype[0].headerstruct 81425825
+datatype[7].documenttype[0].bodystruct -126593034
+datatype[7].documenttype[0].fieldsets{[document]}.fields[0] "child_field"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[1] "rankfeatures"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[2] "summaryfeatures"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[3] "weight"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[4] "weight_src"
diff --git a/config-model/src/test/derived/inheritfromparent/documenttypes.cfg b/config-model/src/test/derived/inheritfromparent/documenttypes.cfg
new file mode 100644
index 00000000000..44677776f3c
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromparent/documenttypes.cfg
@@ -0,0 +1,119 @@
+enablecompression false
+documenttype[0].id 1175161836
+documenttype[0].name "parent"
+documenttype[0].version 0
+documenttype[0].headerstruct 836075987
+documenttype[0].bodystruct -389494616
+documenttype[0].inherits[0].id 8
+documenttype[0].datatype[0].id 1091188812
+documenttype[0].datatype[0].type STRUCT
+documenttype[0].datatype[0].array.element.id 0
+documenttype[0].datatype[0].map.key.id 0
+documenttype[0].datatype[0].map.value.id 0
+documenttype[0].datatype[0].wset.key.id 0
+documenttype[0].datatype[0].wset.createifnonexistent false
+documenttype[0].datatype[0].wset.removeifzero false
+documenttype[0].datatype[0].annotationref.annotation.id 0
+documenttype[0].datatype[0].sstruct.name "parent_struct"
+documenttype[0].datatype[0].sstruct.version 0
+documenttype[0].datatype[0].sstruct.compression.type NONE
+documenttype[0].datatype[0].sstruct.compression.level 0
+documenttype[0].datatype[0].sstruct.compression.threshold 95
+documenttype[0].datatype[0].sstruct.compression.minsize 200
+documenttype[0].datatype[0].sstruct.field[0].name "parent_field"
+documenttype[0].datatype[0].sstruct.field[0].id 933533022
+documenttype[0].datatype[0].sstruct.field[0].id_v6 2116869443
+documenttype[0].datatype[0].sstruct.field[0].datatype 2
+documenttype[0].datatype[1].id 836075987
+documenttype[0].datatype[1].type STRUCT
+documenttype[0].datatype[1].array.element.id 0
+documenttype[0].datatype[1].map.key.id 0
+documenttype[0].datatype[1].map.value.id 0
+documenttype[0].datatype[1].wset.key.id 0
+documenttype[0].datatype[1].wset.createifnonexistent false
+documenttype[0].datatype[1].wset.removeifzero false
+documenttype[0].datatype[1].annotationref.annotation.id 0
+documenttype[0].datatype[1].sstruct.name "parent.header"
+documenttype[0].datatype[1].sstruct.version 0
+documenttype[0].datatype[1].sstruct.compression.type NONE
+documenttype[0].datatype[1].sstruct.compression.level 0
+documenttype[0].datatype[1].sstruct.compression.threshold 95
+documenttype[0].datatype[1].sstruct.compression.minsize 200
+documenttype[0].datatype[1].sstruct.field[0].name "weight_src"
+documenttype[0].datatype[1].sstruct.field[0].id 1225660233
+documenttype[0].datatype[1].sstruct.field[0].id_v6 1350588470
+documenttype[0].datatype[1].sstruct.field[0].datatype 1
+documenttype[0].datatype[1].sstruct.field[1].name "weight"
+documenttype[0].datatype[1].sstruct.field[1].id 1001392207
+documenttype[0].datatype[1].sstruct.field[1].id_v6 1329620545
+documenttype[0].datatype[1].sstruct.field[1].datatype 1
+documenttype[0].datatype[1].sstruct.field[2].name "rankfeatures"
+documenttype[0].datatype[1].sstruct.field[2].id 1883197392
+documenttype[0].datatype[1].sstruct.field[2].id_v6 699950698
+documenttype[0].datatype[1].sstruct.field[2].datatype 2
+documenttype[0].datatype[1].sstruct.field[3].name "summaryfeatures"
+documenttype[0].datatype[1].sstruct.field[3].id 1840337115
+documenttype[0].datatype[1].sstruct.field[3].id_v6 1981648971
+documenttype[0].datatype[1].sstruct.field[3].datatype 2
+documenttype[0].datatype[2].id -389494616
+documenttype[0].datatype[2].type STRUCT
+documenttype[0].datatype[2].array.element.id 0
+documenttype[0].datatype[2].map.key.id 0
+documenttype[0].datatype[2].map.value.id 0
+documenttype[0].datatype[2].wset.key.id 0
+documenttype[0].datatype[2].wset.createifnonexistent false
+documenttype[0].datatype[2].wset.removeifzero false
+documenttype[0].datatype[2].annotationref.annotation.id 0
+documenttype[0].datatype[2].sstruct.name "parent.body"
+documenttype[0].datatype[2].sstruct.version 0
+documenttype[0].datatype[2].sstruct.compression.type NONE
+documenttype[0].datatype[2].sstruct.compression.level 0
+documenttype[0].datatype[2].sstruct.compression.threshold 95
+documenttype[0].datatype[2].sstruct.compression.minsize 200
+documenttype[0].fieldsets{[document]}.fields[0] "weight_src"
+documenttype[1].id 746267614
+documenttype[1].name "child"
+documenttype[1].version 0
+documenttype[1].headerstruct 81425825
+documenttype[1].bodystruct -126593034
+documenttype[1].inherits[0].id 8
+documenttype[1].inherits[1].id 1175161836
+documenttype[1].datatype[0].id 81425825
+documenttype[1].datatype[0].type STRUCT
+documenttype[1].datatype[0].array.element.id 0
+documenttype[1].datatype[0].map.key.id 0
+documenttype[1].datatype[0].map.value.id 0
+documenttype[1].datatype[0].wset.key.id 0
+documenttype[1].datatype[0].wset.createifnonexistent false
+documenttype[1].datatype[0].wset.removeifzero false
+documenttype[1].datatype[0].annotationref.annotation.id 0
+documenttype[1].datatype[0].sstruct.name "child.header"
+documenttype[1].datatype[0].sstruct.version 0
+documenttype[1].datatype[0].sstruct.compression.type NONE
+documenttype[1].datatype[0].sstruct.compression.level 0
+documenttype[1].datatype[0].sstruct.compression.threshold 95
+documenttype[1].datatype[0].sstruct.compression.minsize 200
+documenttype[1].datatype[0].sstruct.field[0].name "child_field"
+documenttype[1].datatype[0].sstruct.field[0].id 1814271363
+documenttype[1].datatype[0].sstruct.field[0].id_v6 405182398
+documenttype[1].datatype[0].sstruct.field[0].datatype 1091188812
+documenttype[1].datatype[1].id -126593034
+documenttype[1].datatype[1].type STRUCT
+documenttype[1].datatype[1].array.element.id 0
+documenttype[1].datatype[1].map.key.id 0
+documenttype[1].datatype[1].map.value.id 0
+documenttype[1].datatype[1].wset.key.id 0
+documenttype[1].datatype[1].wset.createifnonexistent false
+documenttype[1].datatype[1].wset.removeifzero false
+documenttype[1].datatype[1].annotationref.annotation.id 0
+documenttype[1].datatype[1].sstruct.name "child.body"
+documenttype[1].datatype[1].sstruct.version 0
+documenttype[1].datatype[1].sstruct.compression.type NONE
+documenttype[1].datatype[1].sstruct.compression.level 0
+documenttype[1].datatype[1].sstruct.compression.threshold 95
+documenttype[1].datatype[1].sstruct.compression.minsize 200
+documenttype[1].fieldsets{[document]}.fields[0] "child_field"
+documenttype[1].fieldsets{[document]}.fields[1] "rankfeatures"
+documenttype[1].fieldsets{[document]}.fields[2] "summaryfeatures"
+documenttype[1].fieldsets{[document]}.fields[3] "weight"
+documenttype[1].fieldsets{[document]}.fields[4] "weight_src"
diff --git a/config-model/src/test/derived/inheritfromparent/parent.sd b/config-model/src/test/derived/inheritfromparent/parent.sd
new file mode 100644
index 00000000000..d9b0f206109
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromparent/parent.sd
@@ -0,0 +1,17 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search parent {
+ document parent {
+ struct parent_struct {
+ field parent_field type string { }
+ }
+
+ field weight_src type float {
+ }
+ }
+
+ field weight type float {
+ indexing {
+ input weight_src * 10 | attribute | summary;
+ }
+ }
+}
diff --git a/config-model/src/test/derived/inheritfromparent/summarymap.cfg b/config-model/src/test/derived/inheritfromparent/summarymap.cfg
new file mode 100644
index 00000000000..1ab79191ebd
--- /dev/null
+++ b/config-model/src/test/derived/inheritfromparent/summarymap.cfg
@@ -0,0 +1,10 @@
+defaultoutputclass -1
+override[0].field "weight"
+override[0].command "attribute"
+override[0].arguments "weight"
+override[1].field "rankfeatures"
+override[1].command "rankfeatures"
+override[1].arguments ""
+override[2].field "summaryfeatures"
+override[2].command "summaryfeatures"
+override[2].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/inheritstruct/child.sd b/config-model/src/test/derived/inheritstruct/child.sd
new file mode 100644
index 00000000000..05f65edd2ca
--- /dev/null
+++ b/config-model/src/test/derived/inheritstruct/child.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+ document child inherits parent {
+ field child_struct_field type my_struct {
+ indexing: summary | index
+ header
+ match: prefix
+ }
+ }
+}
diff --git a/config-model/src/test/derived/inheritstruct/index-info.cfg b/config-model/src/test/derived/inheritstruct/index-info.cfg
new file mode 100644
index 00000000000..ba9f3eead26
--- /dev/null
+++ b/config-model/src/test/derived/inheritstruct/index-info.cfg
@@ -0,0 +1,21 @@
+indexinfo[0].name "child"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "child_struct_field.my_str"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "child_struct_field.my_str"
+indexinfo[0].command[3].command "lowercase"
+indexinfo[0].command[4].indexname "child_struct_field.my_str"
+indexinfo[0].command[4].command "stem:SHORTEST"
+indexinfo[0].command[5].indexname "child_struct_field.my_str"
+indexinfo[0].command[5].command "normalize"
+indexinfo[0].command[6].indexname "child_struct_field"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "child_struct_field"
+indexinfo[0].command[7].command "lowercase"
+indexinfo[0].command[8].indexname "rankfeatures"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "summaryfeatures"
+indexinfo[0].command[9].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/inheritstruct/parent.sd b/config-model/src/test/derived/inheritstruct/parent.sd
new file mode 100644
index 00000000000..311d039cdb0
--- /dev/null
+++ b/config-model/src/test/derived/inheritstruct/parent.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search parent {
+ document parent {
+ struct my_struct {
+ field my_str type string { }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/integerattributetostringindex/integerattributetostringindex.sd b/config-model/src/test/derived/integerattributetostringindex/integerattributetostringindex.sd
new file mode 100644
index 00000000000..ecca60fd98b
--- /dev/null
+++ b/config-model/src/test/derived/integerattributetostringindex/integerattributetostringindex.sd
@@ -0,0 +1,28 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search integerattributetostringindex {
+ document integerattributetostringindex {
+ field attinx type int {
+ indexing: summary | attribute | index
+ # index-to: attinx,default
+ }
+
+field artist type string {
+indexing: summary | attribute
+# index-to: artist, default
+}
+
+
+field title type string {
+indexing: summary | index
+# index-to: title, default
+}
+
+field year type int {
+indexing: summary | attribute
+# index-to:default
+attribute : fast-search
+}
+
+}
+
+}
diff --git a/config-model/src/test/derived/integerattributetostringindex/summary.cfg b/config-model/src/test/derived/integerattributetostringindex/summary.cfg
new file mode 100644
index 00000000000..87567ceb348
--- /dev/null
+++ b/config-model/src/test/derived/integerattributetostringindex/summary.cfg
@@ -0,0 +1,29 @@
+defaultsummaryid 1195656216
+classes[0].id 1195656216
+classes[0].name "default"
+classes[0].fields[0].name "attinx"
+classes[0].fields[0].type "integer"
+classes[0].fields[1].name "artist"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "title"
+classes[0].fields[2].type "longstring"
+classes[0].fields[3].name "year"
+classes[0].fields[3].type "integer"
+classes[0].fields[4].name "rankfeatures"
+classes[0].fields[4].type "featuredata"
+classes[0].fields[5].name "summaryfeatures"
+classes[0].fields[5].type "featuredata"
+classes[0].fields[6].name "documentid"
+classes[0].fields[6].type "longstring"
+classes[1].id 1706878063
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "attinx"
+classes[1].fields[0].type "integer"
+classes[1].fields[1].name "artist"
+classes[1].fields[1].type "longstring"
+classes[1].fields[2].name "year"
+classes[1].fields[2].type "integer"
+classes[1].fields[3].name "rankfeatures"
+classes[1].fields[3].type "featuredata"
+classes[1].fields[4].name "summaryfeatures"
+classes[1].fields[4].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/attributes.cfg b/config-model/src/test/derived/mail/attributes.cfg
new file mode 100644
index 00000000000..fe6c08cf36d
--- /dev/null
+++ b/config-model/src/test/derived/mail/attributes.cfg
@@ -0,0 +1,40 @@
+attribute[0].name "date"
+attribute[0].datatype INT32
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].loadtype ALWAYS
+attribute[0].sparse false
+attribute[0].noupdate false
+attribute[0].fastsearch false
+attribute[0].fastaggregate false
+attribute[0].huge false
+attribute[0].nosummary false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[1].name "attachmentcount"
+attribute[1].datatype INT32
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].loadtype ALWAYS
+attribute[1].sparse false
+attribute[1].noupdate false
+attribute[1].fastsearch false
+attribute[1].fastaggregate false
+attribute[1].huge false
+attribute[1].nosummary false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8 \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/documentmanager.cfg b/config-model/src/test/derived/mail/documentmanager.cfg
new file mode 100644
index 00000000000..c1d92402ee7
--- /dev/null
+++ b/config-model/src/test/derived/mail/documentmanager.cfg
@@ -0,0 +1,115 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -88808602
+datatype[1].structtype[0].name "mail.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "URI"
+datatype[1].structtype[0].field[0].datatype 10
+datatype[1].structtype[0].field[1].name "mailid"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "date"
+datatype[1].structtype[0].field[2].datatype 0
+datatype[1].structtype[0].field[3].name "from"
+datatype[1].structtype[0].field[3].datatype 2
+datatype[1].structtype[0].field[4].name "replyto"
+datatype[1].structtype[0].field[4].datatype 12
+datatype[1].structtype[0].field[5].name "to"
+datatype[1].structtype[0].field[5].datatype 2
+datatype[1].structtype[0].field[6].name "cc"
+datatype[1].structtype[0].field[6].datatype 2
+datatype[1].structtype[0].field[7].name "bcc"
+datatype[1].structtype[0].field[7].datatype 2
+datatype[1].structtype[0].field[8].name "subject"
+datatype[1].structtype[0].field[8].datatype 2
+datatype[1].structtype[0].field[9].name "snippet"
+datatype[1].structtype[0].field[9].datatype 2
+datatype[1].structtype[0].field[10].name "rankfeatures"
+datatype[1].structtype[0].field[10].datatype 2
+datatype[1].structtype[0].field[11].name "summaryfeatures"
+datatype[1].structtype[0].field[11].datatype 2
+datatype[2].id -1206550296
+datatype[2].arraytype[0].datatype 12
+datatype[3].id -953584901
+datatype[3].structtype[0].name "mail.body"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "body"
+datatype[3].structtype[0].field[0].datatype 12
+datatype[3].structtype[0].field[1].name "attachmentcount"
+datatype[3].structtype[0].field[1].datatype 0
+datatype[3].structtype[0].field[2].name "attachmentnames"
+datatype[3].structtype[0].field[2].datatype 2
+datatype[3].structtype[0].field[3].name "attachmenttypes"
+datatype[3].structtype[0].field[3].datatype 2
+datatype[3].structtype[0].field[4].name "attachmentlanguages"
+datatype[3].structtype[0].field[4].datatype 2
+datatype[3].structtype[0].field[5].name "attachmentcontent"
+datatype[3].structtype[0].field[5].datatype 2
+datatype[3].structtype[0].field[6].name "attachments"
+datatype[3].structtype[0].field[6].datatype -1206550296
+datatype[4].id -1081574983
+datatype[4].documenttype[0].name "mail"
+datatype[4].documenttype[0].version 0
+datatype[4].documenttype[0].inherits[0].name "document"
+datatype[4].documenttype[0].inherits[0].version 0
+datatype[4].documenttype[0].headerstruct -88808602
+datatype[4].documenttype[0].bodystruct -953584901
+datatype[4].documenttype[0].fieldsets{sender}.fields[0] "from"
+datatype[4].documenttype[0].fieldsets{address}.fields[0] "cc"
+datatype[4].documenttype[0].fieldsets{address}.fields[1] "from"
+datatype[4].documenttype[0].fieldsets{address}.fields[2] "to"
+datatype[4].documenttype[0].fieldsets{header}.fields[0] "cc"
+datatype[4].documenttype[0].fieldsets{header}.fields[1] "from"
+datatype[4].documenttype[0].fieldsets{header}.fields[2] "subject"
+datatype[4].documenttype[0].fieldsets{header}.fields[3] "to"
+datatype[4].documenttype[0].fieldsets{default}.fields[0] "body"
+datatype[4].documenttype[0].fieldsets{default}.fields[1] "cc"
+datatype[4].documenttype[0].fieldsets{default}.fields[2] "from"
+datatype[4].documenttype[0].fieldsets{default}.fields[3] "subject"
+datatype[4].documenttype[0].fieldsets{default}.fields[4] "to"
+datatype[4].documenttype[0].fieldsets{all}.fields[0] "attachmentcontent"
+datatype[4].documenttype[0].fieldsets{all}.fields[1] "attachmentnames"
+datatype[4].documenttype[0].fieldsets{all}.fields[2] "attachmenttypes"
+datatype[4].documenttype[0].fieldsets{all}.fields[3] "body"
+datatype[4].documenttype[0].fieldsets{all}.fields[4] "cc"
+datatype[4].documenttype[0].fieldsets{all}.fields[5] "from"
+datatype[4].documenttype[0].fieldsets{all}.fields[6] "subject"
+datatype[4].documenttype[0].fieldsets{all}.fields[7] "to"
+datatype[4].documenttype[0].fieldsets{recipient}.fields[0] "cc"
+datatype[4].documenttype[0].fieldsets{recipient}.fields[1] "to"
+datatype[4].documenttype[0].fieldsets{attachmentname}.fields[0] "attachmentnames"
+datatype[4].documenttype[0].fieldsets{attachmenttype}.fields[0] "attachmenttypes"
+datatype[4].documenttype[0].fieldsets{attachment}.fields[0] "attachmentcontent"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[0] "URI"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[1] "attachmentcontent"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[2] "attachmentcount"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[3] "attachmentlanguages"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[4] "attachmentnames"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[5] "attachments"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[6] "attachmenttypes"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[7] "bcc"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[8] "body"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[9] "cc"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[10] "date"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[11] "from"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[12] "mailid"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[13] "replyto"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[14] "subject"
+datatype[4].documenttype[0].fieldsets{[document]}.fields[15] "to"
diff --git a/config-model/src/test/derived/mail/ilscripts.cfg b/config-model/src/test/derived/mail/ilscripts.cfg
new file mode 100644
index 00000000000..effc8d998b5
--- /dev/null
+++ b/config-model/src/test/derived/mail/ilscripts.cfg
@@ -0,0 +1,33 @@
+ilscript[0].doctype "mail"
+ilscript[0].docfield[0] "URI"
+ilscript[0].docfield[1] "mailid"
+ilscript[0].docfield[2] "date"
+ilscript[0].docfield[3] "from"
+ilscript[0].docfield[4] "replyto"
+ilscript[0].docfield[5] "to"
+ilscript[0].docfield[6] "cc"
+ilscript[0].docfield[7] "bcc"
+ilscript[0].docfield[8] "subject"
+ilscript[0].docfield[9] "body"
+ilscript[0].docfield[10] "attachmentcount"
+ilscript[0].docfield[11] "attachmentnames"
+ilscript[0].docfield[12] "attachmenttypes"
+ilscript[0].docfield[13] "attachmentlanguages"
+ilscript[0].docfield[14] "attachmentcontent"
+ilscript[0].docfield[15] "attachments"
+ilscript[0].content[0] "clear_state | guard { (input body | to_string) . (input attachmentcontent | to_string) | tokenize normalize | summary snippet; }"
+ilscript[0].content[1] "clear_state | guard { input URI | summary URI; }"
+ilscript[0].content[2] "clear_state | guard { input mailid | tokenize normalize | summary mailid | index mailid; }"
+ilscript[0].content[3] "clear_state | guard { input date | summary date | attribute date; }"
+ilscript[0].content[4] "clear_state | guard { input from | tokenize normalize | summary from | index from; }"
+ilscript[0].content[5] "clear_state | guard { input replyto | summary replyto | index replyto; }"
+ilscript[0].content[6] "clear_state | guard { input to | tokenize normalize | summary to | index to; }"
+ilscript[0].content[7] "clear_state | guard { input cc | tokenize normalize | index cc; }"
+ilscript[0].content[8] "clear_state | guard { input bcc | tokenize normalize | index bcc; }"
+ilscript[0].content[9] "clear_state | guard { input subject | tokenize normalize | summary subject | index subject; }"
+ilscript[0].content[10] "clear_state | guard { input body | summary body | index body; }"
+ilscript[0].content[11] "clear_state | guard { input attachmentcount | summary attachmentcount | attribute attachmentcount; }"
+ilscript[0].content[12] "clear_state | guard { input attachmentnames | tokenize normalize | index attachmentnames; }"
+ilscript[0].content[13] "clear_state | guard { input attachmenttypes | tokenize normalize | index attachmenttypes; }"
+ilscript[0].content[14] "clear_state | guard { input attachmentlanguages | tokenize normalize | index attachmentlanguages; }"
+ilscript[0].content[15] "clear_state | guard { input attachmentcontent | tokenize normalize | summary attachmentcontent | index attachmentcontent; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/index-info.cfg b/config-model/src/test/derived/mail/index-info.cfg
new file mode 100644
index 00000000000..fc5192a6014
--- /dev/null
+++ b/config-model/src/test/derived/mail/index-info.cfg
@@ -0,0 +1,155 @@
+indexinfo[0].name "mail"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "URI"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "URI"
+indexinfo[0].command[3].command "fullurl"
+indexinfo[0].command[4].indexname "URI"
+indexinfo[0].command[4].command "lowercase"
+indexinfo[0].command[5].indexname "URI.URI"
+indexinfo[0].command[5].command "fullurl"
+indexinfo[0].command[6].indexname "URI.URI"
+indexinfo[0].command[6].command "lowercase"
+indexinfo[0].command[7].indexname "URI.path"
+indexinfo[0].command[7].command "fullurl"
+indexinfo[0].command[8].indexname "URI.path"
+indexinfo[0].command[8].command "lowercase"
+indexinfo[0].command[9].indexname "URI.query"
+indexinfo[0].command[9].command "fullurl"
+indexinfo[0].command[10].indexname "URI.query"
+indexinfo[0].command[10].command "lowercase"
+indexinfo[0].command[11].indexname "URI.hostname"
+indexinfo[0].command[11].command "urlhost"
+indexinfo[0].command[12].indexname "URI.hostname"
+indexinfo[0].command[12].command "lowercase"
+indexinfo[0].command[13].indexname "mailid"
+indexinfo[0].command[13].command "index"
+indexinfo[0].command[14].indexname "mailid"
+indexinfo[0].command[14].command "lowercase"
+indexinfo[0].command[15].indexname "mailid"
+indexinfo[0].command[15].command "normalize"
+indexinfo[0].command[16].indexname "date"
+indexinfo[0].command[16].command "index"
+indexinfo[0].command[17].indexname "date"
+indexinfo[0].command[17].command "attribute"
+indexinfo[0].command[18].indexname "from"
+indexinfo[0].command[18].command "index"
+indexinfo[0].command[19].indexname "sender"
+indexinfo[0].command[19].command "index"
+indexinfo[0].command[20].indexname "address"
+indexinfo[0].command[20].command "index"
+indexinfo[0].command[21].indexname "header"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "default"
+indexinfo[0].command[22].command "index"
+indexinfo[0].command[23].indexname "all"
+indexinfo[0].command[23].command "index"
+indexinfo[0].command[24].indexname "from"
+indexinfo[0].command[24].command "lowercase"
+indexinfo[0].command[25].indexname "sender"
+indexinfo[0].command[25].command "lowercase"
+indexinfo[0].command[26].indexname "address"
+indexinfo[0].command[26].command "lowercase"
+indexinfo[0].command[27].indexname "header"
+indexinfo[0].command[27].command "lowercase"
+indexinfo[0].command[28].indexname "default"
+indexinfo[0].command[28].command "lowercase"
+indexinfo[0].command[29].indexname "all"
+indexinfo[0].command[29].command "lowercase"
+indexinfo[0].command[30].indexname "from"
+indexinfo[0].command[30].command "normalize"
+indexinfo[0].command[31].indexname "sender"
+indexinfo[0].command[31].command "normalize"
+indexinfo[0].command[32].indexname "address"
+indexinfo[0].command[32].command "normalize"
+indexinfo[0].command[33].indexname "header"
+indexinfo[0].command[33].command "normalize"
+indexinfo[0].command[34].indexname "default"
+indexinfo[0].command[34].command "normalize"
+indexinfo[0].command[35].indexname "all"
+indexinfo[0].command[35].command "normalize"
+indexinfo[0].command[36].indexname "replyto"
+indexinfo[0].command[36].command "index"
+indexinfo[0].command[37].indexname "replyto"
+indexinfo[0].command[37].command "lowercase"
+indexinfo[0].command[38].indexname "replyto"
+indexinfo[0].command[38].command "normalize"
+indexinfo[0].command[39].indexname "to"
+indexinfo[0].command[39].command "index"
+indexinfo[0].command[40].indexname "recipient"
+indexinfo[0].command[40].command "index"
+indexinfo[0].command[41].indexname "to"
+indexinfo[0].command[41].command "lowercase"
+indexinfo[0].command[42].indexname "recipient"
+indexinfo[0].command[42].command "lowercase"
+indexinfo[0].command[43].indexname "to"
+indexinfo[0].command[43].command "normalize"
+indexinfo[0].command[44].indexname "recipient"
+indexinfo[0].command[44].command "normalize"
+indexinfo[0].command[45].indexname "cc"
+indexinfo[0].command[45].command "index"
+indexinfo[0].command[46].indexname "cc"
+indexinfo[0].command[46].command "lowercase"
+indexinfo[0].command[47].indexname "cc"
+indexinfo[0].command[47].command "normalize"
+indexinfo[0].command[48].indexname "bcc"
+indexinfo[0].command[48].command "index"
+indexinfo[0].command[49].indexname "bcc"
+indexinfo[0].command[49].command "lowercase"
+indexinfo[0].command[50].indexname "bcc"
+indexinfo[0].command[50].command "normalize"
+indexinfo[0].command[51].indexname "subject"
+indexinfo[0].command[51].command "index"
+indexinfo[0].command[52].indexname "subject"
+indexinfo[0].command[52].command "lowercase"
+indexinfo[0].command[53].indexname "subject"
+indexinfo[0].command[53].command "normalize"
+indexinfo[0].command[54].indexname "body"
+indexinfo[0].command[54].command "index"
+indexinfo[0].command[55].indexname "body"
+indexinfo[0].command[55].command "lowercase"
+indexinfo[0].command[56].indexname "body"
+indexinfo[0].command[56].command "normalize"
+indexinfo[0].command[57].indexname "attachmentcount"
+indexinfo[0].command[57].command "index"
+indexinfo[0].command[58].indexname "attachmentcount"
+indexinfo[0].command[58].command "attribute"
+indexinfo[0].command[59].indexname "attachmentname"
+indexinfo[0].command[59].command "index"
+indexinfo[0].command[60].indexname "attachmentname"
+indexinfo[0].command[60].command "lowercase"
+indexinfo[0].command[61].indexname "attachmentname"
+indexinfo[0].command[61].command "normalize"
+indexinfo[0].command[62].indexname "attachmenttype"
+indexinfo[0].command[62].command "index"
+indexinfo[0].command[63].indexname "attachmenttype"
+indexinfo[0].command[63].command "lowercase"
+indexinfo[0].command[64].indexname "attachmenttype"
+indexinfo[0].command[64].command "normalize"
+indexinfo[0].command[65].indexname "attachmentlanguages"
+indexinfo[0].command[65].command "index"
+indexinfo[0].command[66].indexname "attachmentlanguages"
+indexinfo[0].command[66].command "lowercase"
+indexinfo[0].command[67].indexname "attachmentlanguages"
+indexinfo[0].command[67].command "normalize"
+indexinfo[0].command[68].indexname "attachment"
+indexinfo[0].command[68].command "index"
+indexinfo[0].command[69].indexname "attachment"
+indexinfo[0].command[69].command "lowercase"
+indexinfo[0].command[70].indexname "attachment"
+indexinfo[0].command[70].command "normalize"
+indexinfo[0].command[71].indexname "attachments"
+indexinfo[0].command[71].command "index"
+indexinfo[0].command[72].indexname "attachments"
+indexinfo[0].command[72].command "multivalue"
+indexinfo[0].command[73].indexname "rankfeatures"
+indexinfo[0].command[73].command "index"
+indexinfo[0].command[74].indexname "snippet"
+indexinfo[0].command[74].command "index"
+indexinfo[0].command[75].indexname "summaryfeatures"
+indexinfo[0].command[75].command "index"
+indexinfo[0].command[76].indexname "snippet"
+indexinfo[0].command[76].command "dynteaser" \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/mail.sd b/config-model/src/test/derived/mail/mail.sd
new file mode 100644
index 00000000000..0010408dbf6
--- /dev/null
+++ b/config-model/src/test/derived/mail/mail.sd
@@ -0,0 +1,99 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search mail {
+
+ stemming: none
+
+ document mail {
+
+ field URI type uri {
+ indexing: summary
+ summary-to: default, mailid
+ }
+
+ field mailid type string {
+ indexing: summary | index
+ match: prefix
+ summary-to: default, mailid
+ }
+
+ field date type int {
+ indexing: summary | attribute | index
+ match: prefix
+ }
+
+ field from type string {
+ indexing: summary | index
+ match: prefix
+ }
+
+ field replyto type string {
+ indexing: summary | index
+ match: prefix
+ }
+
+ field to type string {
+ indexing: summary | index
+ match: prefix
+ }
+
+ field cc type string {
+ indexing: index
+ match: prefix
+ }
+
+ field bcc type string {
+ indexing: index
+ match: prefix
+ }
+
+ field subject type string {
+ indexing: summary | index
+ match: prefix
+ }
+
+ field body type string {
+ indexing: summary | index
+ match: substring
+ body
+ }
+
+ field attachmentcount type int {
+ indexing: summary | index
+ body
+ }
+
+ field attachmentnames type string {
+ indexing: index
+ body
+ }
+
+ field attachmenttypes type string {
+ indexing: index
+ body
+ }
+
+ field attachmentlanguages type string {
+ indexing: index
+ match: prefix
+ body
+ }
+
+ field attachmentcontent type string {
+ indexing: summary | index
+ match: prefix
+ body
+ }
+
+ field attachments type raw[] {
+ body
+ }
+
+ }
+
+ field snippet type string {
+ indexing: (input body | to_string) . (input attachmentcontent | to_string) | summary
+ summary: dynamic
+ }
+
+}
+
diff --git a/config-model/src/test/derived/mail/onlydoc/documentmanager.cfg b/config-model/src/test/derived/mail/onlydoc/documentmanager.cfg
new file mode 100644
index 00000000000..0e95d51bdd8
--- /dev/null
+++ b/config-model/src/test/derived/mail/onlydoc/documentmanager.cfg
@@ -0,0 +1,67 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -88808602
+datatype[1].structtype[0].name "mail.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "URI"
+datatype[1].structtype[0].field[0].datatype 10
+datatype[1].structtype[0].field[1].name "mailid"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "date"
+datatype[1].structtype[0].field[2].datatype 0
+datatype[1].structtype[0].field[3].name "from"
+datatype[1].structtype[0].field[3].datatype 2
+datatype[1].structtype[0].field[4].name "replyto"
+datatype[1].structtype[0].field[4].datatype 2
+datatype[1].structtype[0].field[5].name "to"
+datatype[1].structtype[0].field[5].datatype 2
+datatype[1].structtype[0].field[6].name "cc"
+datatype[1].structtype[0].field[6].datatype 2
+datatype[1].structtype[0].field[7].name "bcc"
+datatype[1].structtype[0].field[7].datatype 2
+datatype[1].structtype[0].field[8].name "subject"
+datatype[1].structtype[0].field[8].datatype 2
+datatype[2].id -1244861287
+datatype[2].arraytype[0].datatype 3
+datatype[3].id -953584901
+datatype[3].structtype[0].name "mail.body"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "body"
+datatype[3].structtype[0].field[0].datatype 2
+datatype[3].structtype[0].field[1].name "attachmentcount"
+datatype[3].structtype[0].field[1].datatype 0
+datatype[3].structtype[0].field[2].name "attachmentnames"
+datatype[3].structtype[0].field[2].datatype 2
+datatype[3].structtype[0].field[3].name "attachmenttypes"
+datatype[3].structtype[0].field[3].datatype 2
+datatype[3].structtype[0].field[4].name "attachmentlanguages"
+datatype[3].structtype[0].field[4].datatype 2
+datatype[3].structtype[0].field[5].name "attachmentcontent"
+datatype[3].structtype[0].field[5].datatype 2
+datatype[3].structtype[0].field[6].name "attachments"
+datatype[3].structtype[0].field[6].datatype -1244861287
+datatype[4].id -1081574983
+datatype[4].documenttype[0].name "mail"
+datatype[4].documenttype[0].version 0
+datatype[4].documenttype[0].inherits[0].name "document"
+datatype[4].documenttype[0].inherits[0].version 0
+datatype[4].documenttype[0].headerstruct -88808602
+datatype[4].documenttype[0].bodystruct -953584901
diff --git a/config-model/src/test/derived/mail/rank-profiles.cfg b/config-model/src/test/derived/mail/rank-profiles.cfg
new file mode 100644
index 00000000000..caca83a9a91
--- /dev/null
+++ b/config-model/src/test/derived/mail/rank-profiles.cfg
@@ -0,0 +1,10 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/summary.cfg b/config-model/src/test/derived/mail/summary.cfg
new file mode 100644
index 00000000000..ac357a934ec
--- /dev/null
+++ b/config-model/src/test/derived/mail/summary.cfg
@@ -0,0 +1,51 @@
+defaultsummaryid 1831052622
+classes[0].id 1831052622
+classes[0].name "default"
+classes[0].fields[0].name "snippet"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "URI"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "mailid"
+classes[0].fields[2].type "longstring"
+classes[0].fields[3].name "date"
+classes[0].fields[3].type "integer"
+classes[0].fields[4].name "from"
+classes[0].fields[4].type "longstring"
+classes[0].fields[5].name "replyto"
+classes[0].fields[5].type "longstring"
+classes[0].fields[6].name "to"
+classes[0].fields[6].type "longstring"
+classes[0].fields[7].name "subject"
+classes[0].fields[7].type "longstring"
+classes[0].fields[8].name "body"
+classes[0].fields[8].type "longstring"
+classes[0].fields[9].name "attachmentcount"
+classes[0].fields[9].type "integer"
+classes[0].fields[10].name "attachmentcontent"
+classes[0].fields[10].type "longstring"
+classes[0].fields[11].name "rankfeatures"
+classes[0].fields[11].type "featuredata"
+classes[0].fields[12].name "summaryfeatures"
+classes[0].fields[12].type "featuredata"
+classes[0].fields[13].name "documentid"
+classes[0].fields[13].type "longstring"
+classes[1].id 1971542976
+classes[1].name "mailid"
+classes[1].fields[0].name "URI"
+classes[1].fields[0].type "longstring"
+classes[1].fields[1].name "mailid"
+classes[1].fields[1].type "longstring"
+classes[1].fields[2].name "rankfeatures"
+classes[1].fields[2].type "featuredata"
+classes[1].fields[3].name "summaryfeatures"
+classes[1].fields[3].type "featuredata"
+classes[2].id 115170470
+classes[2].name "attributeprefetch"
+classes[2].fields[0].name "date"
+classes[2].fields[0].type "integer"
+classes[2].fields[1].name "attachmentcount"
+classes[2].fields[1].type "integer"
+classes[2].fields[2].name "rankfeatures"
+classes[2].fields[2].type "featuredata"
+classes[2].fields[3].name "summaryfeatures"
+classes[2].fields[3].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/summarymap.cfg b/config-model/src/test/derived/mail/summarymap.cfg
new file mode 100644
index 00000000000..2184e43d8eb
--- /dev/null
+++ b/config-model/src/test/derived/mail/summarymap.cfg
@@ -0,0 +1,16 @@
+defaultoutputclass -1
+override[0].field "snippet"
+override[0].command "dynamicteaser"
+override[0].arguments "snippet"
+override[1].field "date"
+override[1].command "attribute"
+override[1].arguments "date"
+override[2].field "attachmentcount"
+override[2].command "attribute"
+override[2].arguments "attachmentcount"
+override[3].field "rankfeatures"
+override[3].command "rankfeatures"
+override[3].arguments ""
+override[4].field "summaryfeatures"
+override[4].command "summaryfeatures"
+override[4].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/vsmfields.cfg b/config-model/src/test/derived/mail/vsmfields.cfg
new file mode 100644
index 00000000000..1006d902cdd
--- /dev/null
+++ b/config-model/src/test/derived/mail/vsmfields.cfg
@@ -0,0 +1,108 @@
+documentverificationlevel 0
+searchall 1
+fieldspec[0].name "URI"
+fieldspec[0].searchmethod AUTOUTF8
+fieldspec[0].arg1 ""
+fieldspec[1].name "mailid"
+fieldspec[1].searchmethod AUTOUTF8
+fieldspec[1].arg1 "prefix"
+fieldspec[2].name "date"
+fieldspec[2].searchmethod INT32
+fieldspec[2].arg1 ""
+fieldspec[3].name "from"
+fieldspec[3].searchmethod AUTOUTF8
+fieldspec[3].arg1 "prefix"
+fieldspec[4].name "replyto"
+fieldspec[4].searchmethod AUTOUTF8
+fieldspec[4].arg1 "prefix"
+fieldspec[5].name "to"
+fieldspec[5].searchmethod AUTOUTF8
+fieldspec[5].arg1 "prefix"
+fieldspec[6].name "cc"
+fieldspec[6].searchmethod AUTOUTF8
+fieldspec[6].arg1 "prefix"
+fieldspec[7].name "bcc"
+fieldspec[7].searchmethod AUTOUTF8
+fieldspec[7].arg1 "prefix"
+fieldspec[8].name "subject"
+fieldspec[8].searchmethod AUTOUTF8
+fieldspec[8].arg1 "prefix"
+fieldspec[9].name "snippet"
+fieldspec[9].searchmethod AUTOUTF8
+fieldspec[9].arg1 ""
+fieldspec[10].name "body"
+fieldspec[10].searchmethod AUTOUTF8
+fieldspec[10].arg1 "substring"
+fieldspec[11].name "attachmentcount"
+fieldspec[11].searchmethod INT32
+fieldspec[11].arg1 ""
+fieldspec[12].name "attachmentnames"
+fieldspec[12].searchmethod AUTOUTF8
+fieldspec[12].arg1 ""
+fieldspec[13].name "attachmenttypes"
+fieldspec[13].searchmethod AUTOUTF8
+fieldspec[13].arg1 ""
+fieldspec[14].name "attachmentlanguages"
+fieldspec[14].searchmethod AUTOUTF8
+fieldspec[14].arg1 "prefix"
+fieldspec[15].name "attachmentcontent"
+fieldspec[15].searchmethod AUTOUTF8
+fieldspec[15].arg1 "prefix"
+documenttype[0].name "mail"
+documenttype[0].index[0].name "mailid"
+documenttype[0].index[0].field[0].name "mailid"
+documenttype[0].index[1].name "date"
+documenttype[0].index[1].field[0].name "date"
+documenttype[0].index[2].name "from"
+documenttype[0].index[2].field[0].name "from"
+documenttype[0].index[3].name "sender"
+documenttype[0].index[3].field[0].name "from"
+documenttype[0].index[4].name "address"
+documenttype[0].index[4].field[0].name "from"
+documenttype[0].index[4].field[1].name "to"
+documenttype[0].index[4].field[2].name "cc"
+documenttype[0].index[5].name "header"
+documenttype[0].index[5].field[0].name "from"
+documenttype[0].index[5].field[1].name "to"
+documenttype[0].index[5].field[2].name "cc"
+documenttype[0].index[5].field[3].name "subject"
+documenttype[0].index[6].name "default"
+documenttype[0].index[6].field[0].name "from"
+documenttype[0].index[6].field[1].name "to"
+documenttype[0].index[6].field[2].name "cc"
+documenttype[0].index[6].field[3].name "subject"
+documenttype[0].index[6].field[4].name "body"
+documenttype[0].index[7].name "all"
+documenttype[0].index[7].field[0].name "from"
+documenttype[0].index[7].field[1].name "to"
+documenttype[0].index[7].field[2].name "cc"
+documenttype[0].index[7].field[3].name "subject"
+documenttype[0].index[7].field[4].name "body"
+documenttype[0].index[7].field[5].name "attachmentnames"
+documenttype[0].index[7].field[6].name "attachmenttypes"
+documenttype[0].index[7].field[7].name "attachmentcontent"
+documenttype[0].index[8].name "replyto"
+documenttype[0].index[8].field[0].name "replyto"
+documenttype[0].index[9].name "to"
+documenttype[0].index[9].field[0].name "to"
+documenttype[0].index[10].name "recipient"
+documenttype[0].index[10].field[0].name "to"
+documenttype[0].index[10].field[1].name "cc"
+documenttype[0].index[11].name "cc"
+documenttype[0].index[11].field[0].name "cc"
+documenttype[0].index[12].name "bcc"
+documenttype[0].index[12].field[0].name "bcc"
+documenttype[0].index[13].name "subject"
+documenttype[0].index[13].field[0].name "subject"
+documenttype[0].index[14].name "body"
+documenttype[0].index[14].field[0].name "body"
+documenttype[0].index[15].name "attachmentcount"
+documenttype[0].index[15].field[0].name "attachmentcount"
+documenttype[0].index[16].name "attachmentname"
+documenttype[0].index[16].field[0].name "attachmentnames"
+documenttype[0].index[17].name "attachmenttype"
+documenttype[0].index[17].field[0].name "attachmenttypes"
+documenttype[0].index[18].name "attachmentlanguages"
+documenttype[0].index[18].field[0].name "attachmentlanguages"
+documenttype[0].index[19].name "attachment"
+documenttype[0].index[19].field[0].name "attachmentcontent" \ No newline at end of file
diff --git a/config-model/src/test/derived/mail/vsmsummary.cfg b/config-model/src/test/derived/mail/vsmsummary.cfg
new file mode 100644
index 00000000000..86de4babe13
--- /dev/null
+++ b/config-model/src/test/derived/mail/vsmsummary.cfg
@@ -0,0 +1,38 @@
+outputclass ""
+fieldmap[0].summary "snippet"
+fieldmap[0].document[0].field "snippet"
+fieldmap[0].command FLATTENJUNIPER
+fieldmap[1].summary "URI"
+fieldmap[1].document[0].field "URI"
+fieldmap[1].command NONE
+fieldmap[2].summary "mailid"
+fieldmap[2].document[0].field "mailid"
+fieldmap[2].command NONE
+fieldmap[3].summary "date"
+fieldmap[3].document[0].field "date"
+fieldmap[3].command NONE
+fieldmap[4].summary "from"
+fieldmap[4].document[0].field "from"
+fieldmap[4].command NONE
+fieldmap[5].summary "replyto"
+fieldmap[5].document[0].field "replyto"
+fieldmap[5].command NONE
+fieldmap[6].summary "to"
+fieldmap[6].document[0].field "to"
+fieldmap[6].command NONE
+fieldmap[7].summary "subject"
+fieldmap[7].document[0].field "subject"
+fieldmap[7].command NONE
+fieldmap[8].summary "body"
+fieldmap[8].document[0].field "body"
+fieldmap[8].command NONE
+fieldmap[9].summary "attachmentcount"
+fieldmap[9].document[0].field "attachmentcount"
+fieldmap[9].command NONE
+fieldmap[10].summary "attachmentcontent"
+fieldmap[10].document[0].field "attachmentcontent"
+fieldmap[10].command NONE
+fieldmap[11].summary "rankfeatures"
+fieldmap[11].command NONE
+fieldmap[12].summary "summaryfeatures"
+fieldmap[12].command NONE \ No newline at end of file
diff --git a/config-model/src/test/derived/mlr/mlr.sd b/config-model/src/test/derived/mlr/mlr.sd
new file mode 100644
index 00000000000..69dad8c30e0
--- /dev/null
+++ b/config-model/src/test/derived/mlr/mlr.sd
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search mlr {
+
+ document mlr {
+
+ field a type string {
+ indexing: index | summary | attribute
+ attribute: prefetch
+ }
+
+ field b type string {
+ indexing: index | summary
+ }
+
+ field ranklog type string {
+ indexing: attribute
+ attribute: prefetch
+ }
+ }
+}
diff --git a/config-model/src/test/derived/mlr/summary.cfg b/config-model/src/test/derived/mlr/summary.cfg
new file mode 100644
index 00000000000..74f5ed567b9
--- /dev/null
+++ b/config-model/src/test/derived/mlr/summary.cfg
@@ -0,0 +1,23 @@
+defaultsummaryid 1868876861
+classes[0].id 1868876861
+classes[0].name "default"
+classes[0].fields[0].name "a"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "b"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "rankfeatures"
+classes[0].fields[2].type "featuredata"
+classes[0].fields[3].name "summaryfeatures"
+classes[0].fields[3].type "featuredata"
+classes[0].fields[4].name "documentid"
+classes[0].fields[4].type "longstring"
+classes[1].id 1944325986
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "a"
+classes[1].fields[0].type "longstring"
+classes[1].fields[1].name "ranklog"
+classes[1].fields[1].type "longstring"
+classes[1].fields[2].name "rankfeatures"
+classes[1].fields[2].type "featuredata"
+classes[1].fields[3].name "summaryfeatures"
+classes[1].fields[3].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/multiplesummaries/attributes.cfg b/config-model/src/test/derived/multiplesummaries/attributes.cfg
new file mode 100644
index 00000000000..5fbe4486849
--- /dev/null
+++ b/config-model/src/test/derived/multiplesummaries/attributes.cfg
@@ -0,0 +1,16 @@
+attribute[5]
+attribute[a].collectiontype SINGLE
+attribute[a].datatype STRING
+attribute[a].name "a"
+attribute[abolded].collectiontype SINGLE
+attribute[abolded].datatype STRING
+attribute[abolded].name "abolded"
+attribute[adynamic].collectiontype SINGLE
+attribute[adynamic].datatype STRING
+attribute[adynamic].name "adynamic"
+attribute[c].collectiontype SINGLE
+attribute[c].datatype STRING
+attribute[c].name "c"
+attribute[loc_pos_zcurve].collectiontype SINGLE
+attribute[loc_pos_zcurve].datatype INT64
+attribute[loc_pos_zcurve].name "loc_pos_zcurve"
diff --git a/config-model/src/test/derived/multiplesummaries/ilscripts.cfg b/config-model/src/test/derived/multiplesummaries/ilscripts.cfg
new file mode 100644
index 00000000000..08b6d0f0b04
--- /dev/null
+++ b/config-model/src/test/derived/multiplesummaries/ilscripts.cfg
@@ -0,0 +1,15 @@
+ilscript[1]
+ilscript[multiplesummaries].doctype "multiplesummaries"
+ilscript[multiplesummaries].name "multiplesummaries"
+ilscript[multiplesummaries].content[11]
+ilscript[multiplesummaries].content[0] "clear_state | guard { input a | tokenize normalize stem:\"SHORTEST\" | summary abolded2 | summary aboldeddynamic | summary adynamic2 | attribute a }"
+ilscript[multiplesummaries].content[1] "clear_state | guard { input adynamic | tokenize normalize stem:\"SHORTEST\" | summary adynamic | attribute adynamic }"
+ilscript[multiplesummaries].content[2] "clear_state | guard { input abolded | tokenize normalize stem:\"SHORTEST\" | summary abolded | attribute abolded }"
+ilscript[multiplesummaries].content[3] "clear_state | guard { input b | summary b }"
+ilscript[multiplesummaries].content[4] "clear_state | guard { input c | summary c | attribute c }"
+ilscript[multiplesummaries].content[5] "clear_state | guard { input d | tokenize normalize stem:\"SHORTEST\" | summary d }"
+ilscript[multiplesummaries].content[6] "clear_state | guard { input e | tokenize normalize stem:\"SHORTEST\" | summary dynamice }"
+ilscript[multiplesummaries].content[7] "clear_state | guard { input f | summary f }"
+ilscript[multiplesummaries].content[8] "clear_state | guard { input g | summary g }"
+ilscript[multiplesummaries].content[9] "clear_state | guard { input h | summary h }"
+ilscript[multiplesummaries].content[10] "clear_state | guard { input loc | to_pos | zcurve | attribute loc_pos_zcurve }"
diff --git a/config-model/src/test/derived/multiplesummaries/index-info.cfg b/config-model/src/test/derived/multiplesummaries/index-info.cfg
new file mode 100644
index 00000000000..5ad04f6c9f2
--- /dev/null
+++ b/config-model/src/test/derived/multiplesummaries/index-info.cfg
@@ -0,0 +1,62 @@
+indexinfo[1]
+indexinfo[multiplesummaries].name "multiplesummaries"
+indexinfo[multiplesummaries].alias[0]
+indexinfo[multiplesummaries].command[29]
+indexinfo[multiplesummaries].command[00].command "index"
+indexinfo[multiplesummaries].command[00].indexname "sddocname"
+indexinfo[multiplesummaries].command[01].command "word"
+indexinfo[multiplesummaries].command[01].indexname "sddocname"
+indexinfo[multiplesummaries].command[02].command "index"
+indexinfo[multiplesummaries].command[02].indexname "a"
+indexinfo[multiplesummaries].command[03].command "attribute"
+indexinfo[multiplesummaries].command[03].indexname "a"
+indexinfo[multiplesummaries].command[04].command "index"
+indexinfo[multiplesummaries].command[04].indexname "adynamic"
+indexinfo[multiplesummaries].command[05].command "attribute"
+indexinfo[multiplesummaries].command[05].indexname "adynamic"
+indexinfo[multiplesummaries].command[06].command "index"
+indexinfo[multiplesummaries].command[06].indexname "abolded"
+indexinfo[multiplesummaries].command[07].command "attribute"
+indexinfo[multiplesummaries].command[07].indexname "abolded"
+indexinfo[multiplesummaries].command[08].command "index"
+indexinfo[multiplesummaries].command[08].indexname "b"
+indexinfo[multiplesummaries].command[09].command "index"
+indexinfo[multiplesummaries].command[09].indexname "c"
+indexinfo[multiplesummaries].command[10].command "attribute"
+indexinfo[multiplesummaries].command[10].indexname "c"
+indexinfo[multiplesummaries].command[11].command "index"
+indexinfo[multiplesummaries].command[11].indexname "d"
+indexinfo[multiplesummaries].command[12].command "index"
+indexinfo[multiplesummaries].command[12].indexname "e"
+indexinfo[multiplesummaries].command[13].command "index"
+indexinfo[multiplesummaries].command[13].indexname "f"
+indexinfo[multiplesummaries].command[14].command "index"
+indexinfo[multiplesummaries].command[14].indexname "g"
+indexinfo[multiplesummaries].command[15].command "index"
+indexinfo[multiplesummaries].command[15].indexname "h"
+indexinfo[multiplesummaries].command[16].command "index"
+indexinfo[multiplesummaries].command[16].indexname "loc"
+indexinfo[multiplesummaries].command[17].command "default-position"
+indexinfo[multiplesummaries].command[17].indexname "loc_pos"
+indexinfo[multiplesummaries].command[18].command "index"
+indexinfo[multiplesummaries].command[18].indexname "loc_pos"
+indexinfo[multiplesummaries].command[19].command "index"
+indexinfo[multiplesummaries].command[19].indexname "loc_pos_zcurve"
+indexinfo[multiplesummaries].command[20].command "attribute"
+indexinfo[multiplesummaries].command[20].indexname "loc_pos_zcurve"
+indexinfo[multiplesummaries].command[21].command "dynteaser"
+indexinfo[multiplesummaries].command[21].indexname "adynamic"
+indexinfo[multiplesummaries].command[22].command "highlight"
+indexinfo[multiplesummaries].command[22].indexname "d"
+indexinfo[multiplesummaries].command[23].command "dynteaser"
+indexinfo[multiplesummaries].command[23].indexname "adynamic2"
+indexinfo[multiplesummaries].command[24].command "highlight"
+indexinfo[multiplesummaries].command[24].indexname "abolded2"
+indexinfo[multiplesummaries].command[25].command "dynteaser"
+indexinfo[multiplesummaries].command[25].indexname "aboldeddynamic"
+indexinfo[multiplesummaries].command[26].command "highlight"
+indexinfo[multiplesummaries].command[26].indexname "aboldeddynamic"
+indexinfo[multiplesummaries].command[27].command "highlight"
+indexinfo[multiplesummaries].command[27].indexname "abolded"
+indexinfo[multiplesummaries].command[28].command "dynteaser"
+indexinfo[multiplesummaries].command[28].indexname "dynamice"
diff --git a/config-model/src/test/derived/multiplesummaries/juniperrc.cfg b/config-model/src/test/derived/multiplesummaries/juniperrc.cfg
new file mode 100755
index 00000000000..79d57c7519c
--- /dev/null
+++ b/config-model/src/test/derived/multiplesummaries/juniperrc.cfg
@@ -0,0 +1,17 @@
+prefix true
+override[3]
+override[abolded].fieldname "abolded"
+override[abolded].length 65536
+override[abolded].max_matches 1
+override[abolded].min_length 8192
+override[abolded].surround_max 65536
+override[abolded2].fieldname "abolded2"
+override[abolded2].length 65536
+override[abolded2].max_matches 1
+override[abolded2].min_length 8192
+override[abolded2].surround_max 65536
+override[d].fieldname "d"
+override[d].length 65536
+override[d].max_matches 1
+override[d].min_length 8192
+override[d].surround_max 65536
diff --git a/config-model/src/test/derived/multiplesummaries/multiplesummaries.sd b/config-model/src/test/derived/multiplesummaries/multiplesummaries.sd
new file mode 100644
index 00000000000..86157133278
--- /dev/null
+++ b/config-model/src/test/derived/multiplesummaries/multiplesummaries.sd
@@ -0,0 +1,199 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search multiplesummaries {
+
+ document multiplesummaries {
+
+ field a type string {
+ indexing: summary | attribute
+ summary-to: default, second
+ match {
+ token
+ }
+ }
+
+ field adynamic type string {
+ indexing: summary | attribute
+ summary-to: default, second
+ summary adynamic: dynamic
+ match {
+ token
+ }
+ }
+
+ field abolded type string {
+ indexing: summary | attribute
+ bolding: on
+ match {
+ token
+ }
+ }
+
+ field b type string {
+ indexing: summary
+ bolding: off
+ }
+
+ field c type string {
+ indexing: summary | attribute
+ summary-to: second
+ match {
+ token
+ }
+ }
+
+ field d type string {
+ indexing: summary
+ bolding: on
+ }
+
+ field e type string {
+ indexing: summary
+ summary dynamice: dynamic
+ }
+
+ field f type array<string> {
+ indexing: summary
+ summary-to: second
+ }
+
+ field g type array<int> {
+ indexing: summary
+ }
+
+ field h type weightedset<string> {
+ indexing: summary
+ }
+
+ field loc type string {
+
+ }
+ }
+
+ field loc_pos type position {
+ indexing: input loc | to_pos | attribute | summary
+ }
+
+ document-summary third {
+
+ summary a type string {
+ }
+
+ summary adynamic type string {
+ }
+
+ summary d type string {
+ }
+
+ summary e type string {
+ }
+
+ summary f type array<string> {
+ }
+
+ summary g type array<int> {
+ }
+
+ summary h type weightedset<string> {
+ }
+
+ }
+
+ document-summary attributesonly1 {
+
+ summary a type string {
+ }
+
+ summary c type string {
+ }
+
+ }
+
+ # Since a here is a dynamic summary field, it will be fetched from disk
+ document-summary notattributesonly1 {
+
+ summary adynamic type string { # Should still be dynamic here
+ }
+
+ summary c type string {
+ }
+
+ }
+
+ # Since a here is a dynamic summary, it will be fetched from disk
+ document-summary notattributesonly2 {
+
+ summary adynamic2 type string { # Should still be dynamic here
+ source: a
+ dynamic
+ }
+
+ summary c type string {
+ }
+
+ }
+
+ # Not attributes only because d is bolded
+ document-summary notattributesonly3 {
+
+ summary a type string {
+ }
+
+ summary d type string {
+ }
+
+ }
+
+ document-summary attributesonly2 {
+
+ summary anotdynamic type string { # Should not be dynamic here
+ source: adynamic
+ }
+
+ summary c type string {
+ }
+
+ summary loc_position type long {
+ }
+
+ }
+
+ document-summary attributesonly3 {
+
+ summary a type string {
+ }
+
+ summary anotbolded type string {
+ source: a
+ }
+
+ summary loc_position type long {
+ }
+
+ }
+
+ document-summary notattributesonly4 {
+
+ summary abolded2 type string {
+ source: a
+ bolding: on
+ }
+
+ summary c type string {
+ }
+
+ }
+
+ document-summary notattributesonly5 {
+
+ summary aboldeddynamic type string {
+ source: a
+ dynamic
+ bolding: on
+ }
+
+ summary c type string {
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/derived/multiplesummaries/summary.cfg b/config-model/src/test/derived/multiplesummaries/summary.cfg
new file mode 100644
index 00000000000..e282afebf39
--- /dev/null
+++ b/config-model/src/test/derived/multiplesummaries/summary.cfg
@@ -0,0 +1,186 @@
+defaultsummaryid 235127765
+classes[12]
+classes[1156201411].id 1156201411
+classes[1156201411].name "attributeprefetch"
+classes[1156201411].fields[5]
+classes[1156201411].fields[a].name "a"
+classes[1156201411].fields[a].type "longstring"
+classes[1156201411].fields[c].name "c"
+classes[1156201411].fields[c].type "longstring"
+classes[1156201411].fields[loc_pos_zcurve].name "loc_pos_zcurve"
+classes[1156201411].fields[loc_pos_zcurve].type "int64"
+classes[1156201411].fields[rankfeatures].name "rankfeatures"
+classes[1156201411].fields[rankfeatures].type "longstring"
+classes[1156201411].fields[summaryfeatures].name "summaryfeatures"
+classes[1156201411].fields[summaryfeatures].type "longstring"
+classes[1277791169].id 1277791169
+classes[1277791169].name "notattributesonly2"
+classes[1277791169].fields[4]
+classes[1277791169].fields[adynamic2].name "adynamic2"
+classes[1277791169].fields[adynamic2].type "longstring"
+classes[1277791169].fields[c].name "c"
+classes[1277791169].fields[c].type "longstring"
+classes[1277791169].fields[rankfeatures].name "rankfeatures"
+classes[1277791169].fields[rankfeatures].type "longstring"
+classes[1277791169].fields[summaryfeatures].name "summaryfeatures"
+classes[1277791169].fields[summaryfeatures].type "longstring"
+classes[1280967808].id 1280967808
+classes[1280967808].name "attributesonly2"
+classes[1280967808].fields[5]
+classes[1280967808].fields[anotdynamic].name "anotdynamic"
+classes[1280967808].fields[anotdynamic].type "longstring"
+classes[1280967808].fields[c].name "c"
+classes[1280967808].fields[c].type "longstring"
+classes[1280967808].fields[loc_position].name "loc_position"
+classes[1280967808].fields[loc_position].type "int64"
+classes[1280967808].fields[rankfeatures].name "rankfeatures"
+classes[1280967808].fields[rankfeatures].type "longstring"
+classes[1280967808].fields[summaryfeatures].name "summaryfeatures"
+classes[1280967808].fields[summaryfeatures].type "longstring"
+classes[1334083320].id 1334083320
+classes[1334083320].name "third"
+classes[1334083320].fields[9]
+classes[1334083320].fields[a].name "a"
+classes[1334083320].fields[a].type "longstring"
+classes[1334083320].fields[adynamic].name "adynamic"
+classes[1334083320].fields[adynamic].type "longstring"
+classes[1334083320].fields[d].name "d"
+classes[1334083320].fields[d].type "longstring"
+classes[1334083320].fields[e].name "e"
+classes[1334083320].fields[e].type "longstring"
+classes[1334083320].fields[f].name "f"
+classes[1334083320].fields[f].type "jsonstring"
+classes[1334083320].fields[g].name "g"
+classes[1334083320].fields[g].type "jsonstring"
+classes[1334083320].fields[h].name "h"
+classes[1334083320].fields[h].type "jsonstring"
+classes[1334083320].fields[rankfeatures].name "rankfeatures"
+classes[1334083320].fields[rankfeatures].type "longstring"
+classes[1334083320].fields[summaryfeatures].name "summaryfeatures"
+classes[1334083320].fields[summaryfeatures].type "longstring"
+classes[1439192258].id 1439192258
+classes[1439192258].name "second"
+classes[1439192258].fields[6]
+classes[1439192258].fields[a].name "a"
+classes[1439192258].fields[a].type "longstring"
+classes[1439192258].fields[adynamic].name "adynamic"
+classes[1439192258].fields[adynamic].type "longstring"
+classes[1439192258].fields[c].name "c"
+classes[1439192258].fields[c].type "longstring"
+classes[1439192258].fields[f].name "f"
+classes[1439192258].fields[f].type "jsonstring"
+classes[1439192258].fields[rankfeatures].name "rankfeatures"
+classes[1439192258].fields[rankfeatures].type "longstring"
+classes[1439192258].fields[summaryfeatures].name "summaryfeatures"
+classes[1439192258].fields[summaryfeatures].type "longstring"
+classes[1653275739].id 1653275739
+classes[1653275739].name "attributesonly3"
+classes[1653275739].fields[5]
+classes[1653275739].fields[a].name "a"
+classes[1653275739].fields[a].type "longstring"
+classes[1653275739].fields[anotbolded].name "anotbolded"
+classes[1653275739].fields[anotbolded].type "longstring"
+classes[1653275739].fields[loc_position].name "loc_position"
+classes[1653275739].fields[loc_position].type "int64"
+classes[1653275739].fields[rankfeatures].name "rankfeatures"
+classes[1653275739].fields[rankfeatures].type "longstring"
+classes[1653275739].fields[summaryfeatures].name "summaryfeatures"
+classes[1653275739].fields[summaryfeatures].type "longstring"
+classes[182001096].id 182001096
+classes[182001096].name "notattributesonly1"
+classes[182001096].fields[4]
+classes[182001096].fields[adynamic].name "adynamic"
+classes[182001096].fields[adynamic].type "longstring"
+classes[182001096].fields[c].name "c"
+classes[182001096].fields[c].type "longstring"
+classes[182001096].fields[rankfeatures].name "rankfeatures"
+classes[182001096].fields[rankfeatures].type "longstring"
+classes[182001096].fields[summaryfeatures].name "summaryfeatures"
+classes[182001096].fields[summaryfeatures].type "longstring"
+classes[1881063334].id 1881063334
+classes[1881063334].name "notattributesonly4"
+classes[1881063334].fields[4]
+classes[1881063334].fields[abolded2].name "abolded2"
+classes[1881063334].fields[abolded2].type "longstring"
+classes[1881063334].fields[c].name "c"
+classes[1881063334].fields[c].type "longstring"
+classes[1881063334].fields[rankfeatures].name "rankfeatures"
+classes[1881063334].fields[rankfeatures].type "longstring"
+classes[1881063334].fields[summaryfeatures].name "summaryfeatures"
+classes[1881063334].fields[summaryfeatures].type "longstring"
+classes[1988966242].id 1988966242
+classes[1988966242].name "attributesonly1"
+classes[1988966242].fields[4]
+classes[1988966242].fields[a].name "a"
+classes[1988966242].fields[a].type "longstring"
+classes[1988966242].fields[c].name "c"
+classes[1988966242].fields[c].type "longstring"
+classes[1988966242].fields[rankfeatures].name "rankfeatures"
+classes[1988966242].fields[rankfeatures].type "longstring"
+classes[1988966242].fields[summaryfeatures].name "summaryfeatures"
+classes[1988966242].fields[summaryfeatures].type "longstring"
+classes[235127765].id 235127765
+classes[235127765].name "multiplesummaries"
+classes[235127765].fields[20]
+classes[235127765].fields[a].name "a"
+classes[235127765].fields[a].type "longstring"
+classes[235127765].fields[abolded].name "abolded"
+classes[235127765].fields[abolded].type "longstring"
+classes[235127765].fields[abolded2].name "abolded2"
+classes[235127765].fields[abolded2].type "longstring"
+classes[235127765].fields[aboldeddynamic].name "aboldeddynamic"
+classes[235127765].fields[aboldeddynamic].type "longstring"
+classes[235127765].fields[adynamic].name "adynamic"
+classes[235127765].fields[adynamic].type "longstring"
+classes[235127765].fields[adynamic2].name "adynamic2"
+classes[235127765].fields[adynamic2].type "longstring"
+classes[235127765].fields[b].name "b"
+classes[235127765].fields[b].type "longstring"
+classes[235127765].fields[c].name "c"
+classes[235127765].fields[c].type "longstring"
+classes[235127765].fields[d].name "d"
+classes[235127765].fields[d].type "longstring"
+classes[235127765].fields[documentid].name "documentid"
+classes[235127765].fields[documentid].type "longstring"
+classes[235127765].fields[dynamice].name "dynamice"
+classes[235127765].fields[dynamice].type "longstring"
+classes[235127765].fields[e].name "e"
+classes[235127765].fields[e].type "longstring"
+classes[235127765].fields[f].name "f"
+classes[235127765].fields[f].type "jsonstring"
+classes[235127765].fields[g].name "g"
+classes[235127765].fields[g].type "jsonstring"
+classes[235127765].fields[h].name "h"
+classes[235127765].fields[h].type "jsonstring"
+classes[235127765].fields[loc_pos.distance].name "loc_pos.distance"
+classes[235127765].fields[loc_pos.distance].type "integer"
+classes[235127765].fields[loc_pos.position].name "loc_pos.position"
+classes[235127765].fields[loc_pos.position].type "xmlstring"
+classes[235127765].fields[loc_position].name "loc_position"
+classes[235127765].fields[loc_position].type "int64"
+classes[235127765].fields[rankfeatures].name "rankfeatures"
+classes[235127765].fields[rankfeatures].type "longstring"
+classes[235127765].fields[summaryfeatures].name "summaryfeatures"
+classes[235127765].fields[summaryfeatures].type "longstring"
+classes[803323247].id 803323247
+classes[803323247].name "notattributesonly3"
+classes[803323247].fields[4]
+classes[803323247].fields[a].name "a"
+classes[803323247].fields[a].type "longstring"
+classes[803323247].fields[d].name "d"
+classes[803323247].fields[d].type "longstring"
+classes[803323247].fields[rankfeatures].name "rankfeatures"
+classes[803323247].fields[rankfeatures].type "longstring"
+classes[803323247].fields[summaryfeatures].name "summaryfeatures"
+classes[803323247].fields[summaryfeatures].type "longstring"
+classes[937467944].id 937467944
+classes[937467944].name "notattributesonly5"
+classes[937467944].fields[4]
+classes[937467944].fields[aboldeddynamic].name "aboldeddynamic"
+classes[937467944].fields[aboldeddynamic].type "longstring"
+classes[937467944].fields[c].name "c"
+classes[937467944].fields[c].type "longstring"
+classes[937467944].fields[rankfeatures].name "rankfeatures"
+classes[937467944].fields[rankfeatures].type "longstring"
+classes[937467944].fields[summaryfeatures].name "summaryfeatures"
+classes[937467944].fields[summaryfeatures].type "longstring"
diff --git a/config-model/src/test/derived/multiplesummaries/summarymap.cfg b/config-model/src/test/derived/multiplesummaries/summarymap.cfg
new file mode 100644
index 00000000000..430715062c8
--- /dev/null
+++ b/config-model/src/test/derived/multiplesummaries/summarymap.cfg
@@ -0,0 +1,50 @@
+defaultoutputclass -1
+override[16]
+override[a].arguments "a"
+override[a].command "attribute"
+override[a].field "a"
+override[abolded].arguments "abolded"
+override[abolded].command "dynamicteaser"
+override[abolded].field "abolded"
+override[abolded2].arguments "abolded2"
+override[abolded2].command "dynamicteaser"
+override[abolded2].field "abolded2"
+override[aboldeddynamic].arguments "aboldeddynamic"
+override[aboldeddynamic].command "dynamicteaser"
+override[aboldeddynamic].field "aboldeddynamic"
+override[adynamic].arguments "adynamic"
+override[adynamic].command "dynamicteaser"
+override[adynamic].field "adynamic"
+override[adynamic2].arguments "adynamic2"
+override[adynamic2].command "dynamicteaser"
+override[adynamic2].field "adynamic2"
+override[anotbolded].arguments "a"
+override[anotbolded].command "attribute"
+override[anotbolded].field "anotbolded"
+override[anotdynamic].arguments "adynamic"
+override[anotdynamic].command "attribute"
+override[anotdynamic].field "anotdynamic"
+override[c].arguments "c"
+override[c].command "attribute"
+override[c].field "c"
+override[d].arguments "d"
+override[d].command "dynamicteaser"
+override[d].field "d"
+override[dynamice].arguments "dynamice"
+override[dynamice].command "dynamicteaser"
+override[dynamice].field "dynamice"
+override[loc_pos.distance].arguments "loc_pos_zcurve"
+override[loc_pos.distance].command "absdist"
+override[loc_pos.distance].field "loc_pos.distance"
+override[loc_pos.position].arguments "loc_pos_zcurve"
+override[loc_pos.position].command "positions"
+override[loc_pos.position].field "loc_pos.position"
+override[loc_pos_zcurve].arguments "loc_pos_zcurve"
+override[loc_pos_zcurve].command "attribute"
+override[loc_pos_zcurve].field "loc_pos_zcurve"
+override[rankfeatures].arguments ""
+override[rankfeatures].command "rankfeatures"
+override[rankfeatures].field "rankfeatures"
+override[summaryfeatures].arguments ""
+override[summaryfeatures].command "summaryfeatures"
+override[summaryfeatures].field "summaryfeatures"
diff --git a/config-model/src/test/derived/music/attributes.cfg b/config-model/src/test/derived/music/attributes.cfg
new file mode 100644
index 00000000000..5ded3dc494d
--- /dev/null
+++ b/config-model/src/test/derived/music/attributes.cfg
@@ -0,0 +1,209 @@
+attribute[0].name "sales"
+attribute[0].datatype INT32
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "pto"
+attribute[1].datatype INT32
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "mid"
+attribute[2].datatype INT32
+attribute[2].collectiontype SINGLE
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending true
+attribute[2].sortfunction UCA
+attribute[2].sortstrength PRIMARY
+attribute[2].sortlocale ""
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype ""
+attribute[3].name "weight"
+attribute[3].datatype FLOAT
+attribute[3].collectiontype SINGLE
+attribute[3].removeifzero false
+attribute[3].createifnonexistent false
+attribute[3].fastsearch false
+attribute[3].huge false
+attribute[3].sortascending true
+attribute[3].sortfunction UCA
+attribute[3].sortstrength PRIMARY
+attribute[3].sortlocale ""
+attribute[3].enablebitvectors false
+attribute[3].enableonlybitvector false
+attribute[3].fastaccess false
+attribute[3].arity 8
+attribute[3].lowerbound -9223372036854775808
+attribute[3].upperbound 9223372036854775807
+attribute[3].densepostinglistthreshold 0.4
+attribute[3].tensortype ""
+attribute[4].name "bgnpfrom"
+attribute[4].datatype FLOAT
+attribute[4].collectiontype SINGLE
+attribute[4].removeifzero false
+attribute[4].createifnonexistent false
+attribute[4].fastsearch false
+attribute[4].huge false
+attribute[4].sortascending true
+attribute[4].sortfunction UCA
+attribute[4].sortstrength PRIMARY
+attribute[4].sortlocale ""
+attribute[4].enablebitvectors false
+attribute[4].enableonlybitvector false
+attribute[4].fastaccess false
+attribute[4].arity 8
+attribute[4].lowerbound -9223372036854775808
+attribute[4].upperbound 9223372036854775807
+attribute[4].densepostinglistthreshold 0.4
+attribute[4].tensortype ""
+attribute[5].name "newestedition"
+attribute[5].datatype INT32
+attribute[5].collectiontype SINGLE
+attribute[5].removeifzero false
+attribute[5].createifnonexistent false
+attribute[5].fastsearch false
+attribute[5].huge false
+attribute[5].sortascending true
+attribute[5].sortfunction UCA
+attribute[5].sortstrength PRIMARY
+attribute[5].sortlocale ""
+attribute[5].enablebitvectors false
+attribute[5].enableonlybitvector false
+attribute[5].fastaccess false
+attribute[5].arity 8
+attribute[5].lowerbound -9223372036854775808
+attribute[5].upperbound 9223372036854775807
+attribute[5].densepostinglistthreshold 0.4
+attribute[5].tensortype ""
+attribute[6].name "year"
+attribute[6].datatype INT32
+attribute[6].collectiontype SINGLE
+attribute[6].removeifzero false
+attribute[6].createifnonexistent false
+attribute[6].fastsearch false
+attribute[6].huge false
+attribute[6].sortascending true
+attribute[6].sortfunction UCA
+attribute[6].sortstrength PRIMARY
+attribute[6].sortlocale ""
+attribute[6].enablebitvectors false
+attribute[6].enableonlybitvector false
+attribute[6].fastaccess false
+attribute[6].arity 8
+attribute[6].lowerbound -9223372036854775808
+attribute[6].upperbound 9223372036854775807
+attribute[6].densepostinglistthreshold 0.4
+attribute[6].tensortype ""
+attribute[7].name "did"
+attribute[7].datatype INT32
+attribute[7].collectiontype SINGLE
+attribute[7].removeifzero false
+attribute[7].createifnonexistent false
+attribute[7].fastsearch false
+attribute[7].huge false
+attribute[7].sortascending true
+attribute[7].sortfunction UCA
+attribute[7].sortstrength PRIMARY
+attribute[7].sortlocale ""
+attribute[7].enablebitvectors false
+attribute[7].enableonlybitvector false
+attribute[7].fastaccess false
+attribute[7].arity 8
+attribute[7].lowerbound -9223372036854775808
+attribute[7].upperbound 9223372036854775807
+attribute[7].densepostinglistthreshold 0.4
+attribute[7].tensortype ""
+attribute[8].name "cbid"
+attribute[8].datatype INT32
+attribute[8].collectiontype SINGLE
+attribute[8].removeifzero false
+attribute[8].createifnonexistent false
+attribute[8].fastsearch false
+attribute[8].huge false
+attribute[8].sortascending true
+attribute[8].sortfunction UCA
+attribute[8].sortstrength PRIMARY
+attribute[8].sortlocale ""
+attribute[8].enablebitvectors false
+attribute[8].enableonlybitvector false
+attribute[8].fastaccess false
+attribute[8].arity 8
+attribute[8].lowerbound -9223372036854775808
+attribute[8].upperbound 9223372036854775807
+attribute[8].densepostinglistthreshold 0.4
+attribute[8].tensortype ""
+attribute[9].name "hiphopvalue_arr"
+attribute[9].datatype STRING
+attribute[9].collectiontype ARRAY
+attribute[9].removeifzero false
+attribute[9].createifnonexistent false
+attribute[9].fastsearch false
+attribute[9].huge false
+attribute[9].sortascending true
+attribute[9].sortfunction UCA
+attribute[9].sortstrength PRIMARY
+attribute[9].sortlocale ""
+attribute[9].enablebitvectors false
+attribute[9].enableonlybitvector false
+attribute[9].fastaccess false
+attribute[9].arity 8
+attribute[9].lowerbound -9223372036854775808
+attribute[9].upperbound 9223372036854775807
+attribute[9].densepostinglistthreshold 0.4
+attribute[9].tensortype ""
+attribute[10].name "metalvalue_arr"
+attribute[10].datatype STRING
+attribute[10].collectiontype ARRAY
+attribute[10].removeifzero false
+attribute[10].createifnonexistent false
+attribute[10].fastsearch false
+attribute[10].huge false
+attribute[10].sortascending true
+attribute[10].sortfunction UCA
+attribute[10].sortstrength PRIMARY
+attribute[10].sortlocale ""
+attribute[10].enablebitvectors false
+attribute[10].enableonlybitvector false
+attribute[10].fastaccess false
+attribute[10].arity 8
+attribute[10].lowerbound -9223372036854775808
+attribute[10].upperbound 9223372036854775807
+attribute[10].densepostinglistthreshold 0.4
+attribute[10].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/music/defs/attributes.def b/config-model/src/test/derived/music/defs/attributes.def
new file mode 100644
index 00000000000..bb3a0df6299
--- /dev/null
+++ b/config-model/src/test/derived/music/defs/attributes.def
@@ -0,0 +1,7 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+attribute[].name string
+attribute[].datatype string
+attribute[].multivalue bool default=false
+attribute[].sortsigned bool default=true
+attribute[].disableprep bool default=false
diff --git a/config-model/src/test/derived/music/defs/documentmanager.def b/config-model/src/test/derived/music/defs/documentmanager.def
new file mode 100644
index 00000000000..a310e9f13c8
--- /dev/null
+++ b/config-model/src/test/derived/music/defs/documentmanager.def
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=document.config
+datatype[].id int
+datatype[].arraytype[].datatype int
+documenttype[].name string
+documenttype[].version int
+documenttype[].inherits[].name string
+documenttype[].inherits[].version int
+documenttype[].field[].name string
+documenttype[].field[].id int
+documenttype[].field[].header bool
+documenttype[].field[].datatype int
diff --git a/config-model/src/test/derived/music/defs/extra.def b/config-model/src/test/derived/music/defs/extra.def
new file mode 100644
index 00000000000..cc03f1b39d3
--- /dev/null
+++ b/config-model/src/test/derived/music/defs/extra.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=test
+file[].name string
+file[].content string
diff --git a/config-model/src/test/derived/music/defs/ilscripts.def b/config-model/src/test/derived/music/defs/ilscripts.def
new file mode 100644
index 00000000000..d999742fa3c
--- /dev/null
+++ b/config-model/src/test/derived/music/defs/ilscripts.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.configdefinition
+ilscript[].name string
+ilscript[].doctype string
+ilscript[].content[] string
diff --git a/config-model/src/test/derived/music/defs/rank-profiles.def b/config-model/src/test/derived/music/defs/rank-profiles.def
new file mode 100644
index 00000000000..0d7cf27ff06
--- /dev/null
+++ b/config-model/src/test/derived/music/defs/rank-profiles.def
@@ -0,0 +1,344 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+## name of this rank profile. maps to table index for internal use.
+rankprofile[].name string
+
+## the name of a generic property available to the feature execution framework and feature plugins
+rankprofile[].fef.property[].name string
+
+## the value of a generic property available to feature plugins
+rankprofile[].fef.property[].value string
+
+## the catalog name overrides apply to
+rankprofile[].catalog[].name string
+
+## Boost value for AND queries in this catalog.
+rankprofile[].catalog[].andboost int default=0
+
+## Boost value for OR queries in this catalog.
+rankprofile[].catalog[].orboost int default=0
+
+## Boost value for ANY queries in this catalog.
+rankprofile[].catalog[].anyboost int default=0
+
+## Boost value for NEAR queries in catalog.
+rankprofile[].catalog[].nearboost int default=0
+
+## Boost value for ORDEREDNEAR queries in this catalog.
+rankprofile[].catalog[].orderednearboost int default=0
+
+## Boost value for phrase queries in this catalog.
+rankprofile[].catalog[].phraseboost int default=0
+
+## Boost value for all queries in catalog.
+rankprofile[].catalog[].rankboost int default=0
+
+## If true, the context boost is the max value of
+## the individual contextboosts.
+## When false, the context boost when a term is in
+## several contexts is the sum of the individual contextboosts.
+rankprofile[].catalog[].bestcontextboostonly bool default=false
+
+
+## If true, then use extnumoccboost only when calculating rank values.
+## Also, do not normalize the extnumoccboost value with
+## global term frequency. Default value is false.
+rankprofile[].catalog[].extnumoccboostonly bool default=false
+
+## If yes, then use extnumoccboost only when calculating rank values.
+## Also, do not normalize the extnumoccboost value with
+## global term frequency. Default value is no.
+rankprofile[].catalog[].numoccandextnumoccboostonly bool default=false
+
+## If yes, then use bitvectors when possible.
+## Default value is false.
+rankprofile[].catalog[].preferbitvector bool default=false
+
+## Load extnumoccboost for this catalog from the named file.
+## extnumoccboost specifies boost values due to the number of
+## occurences of a term that are external to the document. If
+## "NULL" is given as file name, then all extnumoccboost values
+## will be set to 0.
+rankprofile[].catalog[].extnumoccboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load numoccboost for this catalog from the named file.
+## numoccboost specifies boost values due to the number of occurences in
+## a document. If "NULL" is given as file name, then all numoccboost
+## values will be set to 0.
+rankprofile[].catalog[].numoccboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load firstoccboost for catalog from the file named.
+## firstoccboost specifies boost values due to the position of the
+## first occurence in a document. If "NULL" is given as file name,
+## then all firstoccboost values will be set to 0.
+rankprofile[].catalog[].firstoccboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+
+## Load firstoccproximityboost for this catalog from the file named.
+## firstoccproximity boost specifies boost values due to the correlation between
+## positions of the first occurence in a document for two and two words.
+##
+## If "NULL" is given as file name, then all
+## firstoccproximityboost values will be set to 0. If otherwise set,
+## should be the name of a file to load into the table. The file
+## should have 256 lines each containing a single integer.
+##
+## There are 256 elements in the table, handling forward distances from 1.
+## The corresponding firstoccrevproximityboost table is used
+## to handle closeness in reverse order.
+##
+## The last array index specifies the proximity table set. During
+## evaluation, the bigram proximity weight supplied by the query segmenter
+## specifies which proximity table set to use, with a fallback to set 0
+## when no information is available.
+rankprofile[].catalog[].firstoccproximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load firstoccrevproximityboost table for this catalog from the named file.
+## Specifies boost values due to the correlation between positions
+## of the first occurence in a document for two and two words when
+## the second word in the query comes first in the document.
+## See also firstoccproximityboost above.
+rankprofile[].catalog[].firstoccrevproximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load proximityboost for this catalog from the named file.
+## proximity boost specifies boost values due to the correlation between
+## positions of the occurences in a document for two and two words.
+## See also firstoccproximityboost above.
+rankprofile[].catalog[].proximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load revproximityboost for this catalog from the named file.
+## revproximity boost specifies boost values due to the correlation between
+## positions of the occurences in a document for two and two words.
+## See also firstoccproximityboost above.
+rankprofile[].catalog[].revproximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load divtable for this catalog from the named file.
+## Rank values for a query term are divided by the entry
+## in divtable indexed by log2 of term frequence.
+## The file should contain ?? lines each with a single integer.
+rankprofile[].catalog[].divtable string default=""
+
+## The name of a context in this catalog to specify boosts for.
+rankprofile[].catalog[].context[].name string
+
+## Boost occurrences in this context with the given value.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].contextboost int default=0
+
+## Boost pair of occurrences in this context with
+## the given value when evaluating 2 words from same catalog in
+## parallell.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].commoncontextboost.pair int default=0
+
+## Boost triple of occurrences in this context with
+## the given value when evaluating 3 words from same catalog in
+## parallell.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].commoncontextboost.triple int default=0
+
+## Boost quad of occurrences in this context with
+## the given value when evaluating 4 words from same catalog in
+## parallell.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].commoncontextboost.quad int default=0
+
+
+## The name of the attribute
+rankprofile[].attribute[].name string
+
+## Boost value for queries that hit in this attribute
+rankprofile[].attribute[].attributecontextboost int default=0
+
+## Load weightboost for this attribute from the named file.
+## weightboost specifies boost values due to the weight (weighted set)
+## or number of occurences (single, array) in an attribute.
+## If "NULL" is given as file name, then all weightboost values will be set to 0.
+rankprofile[].attribute[].weightboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+
+## Load static rank values from the given staticrank docattr vector.
+## Must be specified in index.cf as a staticrankfile.
+rankprofile[].staticrankfile string default=""
+
+## Multiply static rank values with given value when calculating total
+## rank value.
+rankprofile[].staticcoefficient int default=1
+
+## If false then use only static ranking when sorting result hits.
+## Default is true.
+rankprofile[].dynamicranking bool default=true
+
+## If dynamic ranking is turned off, then ascending will sort the
+## result hits with lowest static rank values first, while
+## descending will sort with highest static rank values
+## first. Default is descending. This keyword has no effect if
+## dynamic ranking is on.
+rankprofile[].staticranksortorder string default="descending"
+
+## Load static rank mapping from the file named table. The static
+## rank mapping maps each 8-bit static rank value into a 32-bit static
+## rank value. This option may only be used with 8-bit static rank files.
+rankprofile[].staticrankmap string default=""
+
+## If set to "true", total rank will be reduced when dynamic rank is less than
+## 25% of static rank, to suppress irrelevant hits from popular sites.
+## If set to "false", total rank is not reduced.
+rankprofile[].clampstaticrank bool default=false
+
+## Load document datetime values used for freshness boost calculation from
+## this file. The values must be coded as minutes since
+## 1900-01-01T00:00Z. The value 0 has the special meaning
+## "no datetime value exists".
+rankprofile[].freshnessboost.file string default=""
+
+## Load freshnessboost lookup-table values from the file named
+## table instead of using built-in default values. The file must
+## contain 32 white-space separated non-negative integers.
+rankprofile[].freshnessboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## When calculating the freshness boost value multiply difference between
+## current datetime and document datetime with timeoffset before taking
+## the base-2 logarithm. Default value is 1. Max value is 31.
+rankprofile[].freshnessboost.timeoffset int default=1
+
+## If a document has datetime value 0, then use defaultboostvalue
+## as freshness boost value instead of doing table lookup. The default
+## default value is 0 (no boost).
+rankprofile[].freshnessboost.defaultboostvalue int default=0
+
+## Multiply freshness boost value with coefficient when calculating
+## total freshness boost value. If coefficient 0 is used, no freshness
+## boost value will be computed or added. Default value is 0.
+rankprofile[].freshnessboost.coefficient int default=0
+
+## boost table files for distance ranking, 1 dimension.
+## The tables have 465 elements each, where slots 0..15 represents
+## distances 0..15 while the remaining slots represents distance
+## (16 + (slot & 15)) << ((slot >> 4) - 1). Linear interpolation is
+## used for distances "between" table slots.
+##
+## If "NULL" is given as the file name then all 1D distance boost values
+## for that table will be set to 0.
+rankprofile[].distance1dboosttable[].table string
+
+## boost table files for distance ranking, 2 dimensions.
+## The tables have 977 elements each, where slots 0..15 represents
+## square of distance being 0..15 while the remaining slots represents
+## square of distance distance being
+## (16 + (slot & 15)) << ((slot >> 4) - 1). Linear interpolation is
+## used for distances "between" table slots.
+##
+## If "NULL" is given as the file name then all 2D distance boost values
+## for that table will be set to 0.
+rankprofile[].distance2dboosttable[].table string
+
+## The lowest possible size of a ranked result. This is the lower ramp
+## of the percentage specified in the binsize variable. The default is
+## specified in fsearchrc.
+rankprofile[].binlow int default=-1
+
+## The high limit of the ranked result bin. If the percentage of the
+## resultset specified in binsize is higher than this limit, this will be
+## the max size. The default is specified in fsearchrc.
+rankprofile[].binhigh int default=-1
+
+## The size of the ranked results as a percentage of the total result
+## set size. The percentage can be ramped off with the binlow and binhigh
+## variables. The default is specified in fsearchrc.
+rankprofile[].binsize double default=-1
+
+## Minimum value for maximum value of number of 'posocc' entries for a word.
+## The default is specified in fsearchrc.
+rankprofile[].posbinlow int default=-1
+
+## Maximum value for maximum value of number of 'posocc' entries for a word.
+## The default is specified in fsearchrc.
+rankprofile[].posbinhigh int default=-1
+
+## The maximum value for number of 'posocc' entries for a word, specified
+## as a percentage of the number of documents in the index. If more
+## entries are needed for evaluation, posocc entries are not used for that
+## word and evaluation will be performed without full proximity support.
+## The percentage can be ramped off with the posbinlow and posbinhigh
+## variables. The default is specified in fsearchrc.
+rankprofile[].posbinsize int default=-1
+
+## Boost value that is added to the relevance score of hits from superior
+## searches (searches where recall is sacrificed for better
+## precision). The rank cutoff feature will not be affected by this
+## feature (rank cutoff is applied before the superior boost).
+rankprofile[].superiorboost int default=0
+
+## Name of rank profile to be used when running superior searches
+## (searches where recall is sacrificed for better precision). If not
+## set, the current ranking profile will be used.
+##
+## If a profile for a superior search has this set then a superior^2
+## search exist (with more recall sacrificed than for superior searches)
+## and search behavior is slightly changed:
+##
+## If the search node has been asked to perform a superior search then an
+## internal double fallthrough with (superior, superior^2) search is
+## performed. If the search node has been asked to perform an internal
+## double fallthrough then a triple fallthrough with (normal, superior,
+## superior^2) is performed.
+rankprofile[].superiorname string default=""
+
+## After all other rank calculations, the rank value is tuned according
+## to the tunefactor and tunebias values. The rank value is modified
+## as follows: new_rank = old_rank * tunefactor + tunebias.
+rankprofile[].tunefactor double default=1.0
+
+## After all other rank calculations, the rank value is tuned according
+## to the tunefactor and tunebias values. The rank value is modified
+## as follows: new_rank = old_rank * tunefactor + tunebias.
+rankprofile[].tunebias int default=0
+
+## A lower limit for the rankvalue of the results returned from the
+## search node. If rankcutoff.advanced is set to "true", determines
+## the constant value used in the internal advanced rank cutoff
+## calculations. This roughly reflects the expected rank contribution
+## of one good term.
+## The rankcutoff.val value and the rankcutoff.advanced parameter
+## may be used if you only want hits with a minimum relevancy to show
+## up in the resultset.
+## A value below zero means no rankcutoff is done.
+rankprofile[].rankcutoff.val int default=-1
+
+## When rankcutoff.val is in use, this flag controls whether to use
+## an internal calculation is used for determining the rank cutoff
+## value. If "false", use rankcutoff.val as a direct lower limit.
+rankprofile[].rankcutoff.advanced bool default=false
+
+## If set to "ON", use of posocc files is enabled, except when
+## "forceemptyposoccs" is set in fsearchrc or posocc files doesn't exist.
+## If set to "OFF", use of posocc files is disabled.
+## If "NOTSET" the fsearchrc "proximity" parameter is used instead.
+rankprofile[].proximity.full.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## If set to "ON", use of firstoccproximity is enabled.
+## If set to "OFF", use of firstoccproximity is disabled.
+## When NOTSET use the firstoccproximity value in fsearchrc configuration.
+rankprofile[].proximity.firstocc.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## If set to "ON", use of proximity (cf. proximity and firstoccproximity)
+## will affect phrases in addition to single words.
+## If set to "OFF", proximity is never used for phrases.
+## When NOTSET use the phraseproximity value in fsearchrc configuration.
+rankprofile[].proximity.phrase.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## Selects behavior when proximity can be used for two words but not three
+## words while firstoccproximity can be used for three words.
+## If set to "ON", then use proximity for two words.
+## If set to "OFF", then use firstoccproximity for three words.
+## When NOTSET use the proximitypairbeforefirstoccproximitytriple value
+## in fsearchrc configuration.
+rankprofile[].proximity.pairbeforefirstocctriple.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## Selects behavior when proximity can be used for three words but not four
+## words while firstoccproximity can be used for four words.
+## If set to "ON", then use proximity for three words.
+## If set to "OFF", then use firstoccproximity for four words.
+## When NOTSET use the proximitytriplebeforefirstoccproximityquad value
+rankprofile[].proximity.triplebeforefirstoccquad.enable enum { OFF, ON, NOTSET } default=NOTSET
diff --git a/config-model/src/test/derived/music/defs/summarymap.def b/config-model/src/test/derived/music/defs/summarymap.def
new file mode 100644
index 00000000000..7bb5e7df849
--- /dev/null
+++ b/config-model/src/test/derived/music/defs/summarymap.def
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+
+## The default output summary class id, -1 to use the stored summary class id
+defaultoutputclass int default=-1
+
+## The name of the summary field which is overridden with a dynamic value
+override[].field string
+
+## The overriding command, one of:
+## staticrank, dynamicteaser, dynamicteasermetric, label, ranklog, empty, copy
+override[].command string
+
+## Space-seaparated arguments, dedendent on the given command
+override[].arguments string default=""
diff --git a/config-model/src/test/derived/music/ilscripts.cfg b/config-model/src/test/derived/music/ilscripts.cfg
new file mode 100644
index 00000000000..ca2c35ffcba
--- /dev/null
+++ b/config-model/src/test/derived/music/ilscripts.cfg
@@ -0,0 +1,76 @@
+maxtermoccurrences 100
+ilscript[0].doctype "music"
+ilscript[0].docfield[0] "bgndata"
+ilscript[0].docfield[1] "sales"
+ilscript[0].docfield[2] "pto"
+ilscript[0].docfield[3] "keys"
+ilscript[0].docfield[4] "mid"
+ilscript[0].docfield[5] "ew"
+ilscript[0].docfield[6] "surl"
+ilscript[0].docfield[7] "userrate"
+ilscript[0].docfield[8] "pid"
+ilscript[0].docfield[9] "weight"
+ilscript[0].docfield[10] "url"
+ilscript[0].docfield[11] "isbn"
+ilscript[0].docfield[12] "fmt"
+ilscript[0].docfield[13] "albumid"
+ilscript[0].docfield[14] "disp_song"
+ilscript[0].docfield[15] "song"
+ilscript[0].docfield[16] "pfrom"
+ilscript[0].docfield[17] "bgnpfrom"
+ilscript[0].docfield[18] "categories"
+ilscript[0].docfield[19] "data"
+ilscript[0].docfield[20] "numreview"
+ilscript[0].docfield[21] "bgnsellers"
+ilscript[0].docfield[22] "image"
+ilscript[0].docfield[23] "artist"
+ilscript[0].docfield[24] "artistspid"
+ilscript[0].docfield[25] "title"
+ilscript[0].docfield[26] "newestedition"
+ilscript[0].docfield[27] "bgnpto"
+ilscript[0].docfield[28] "year"
+ilscript[0].docfield[29] "did"
+ilscript[0].docfield[30] "scorekey"
+ilscript[0].docfield[31] "cbid"
+ilscript[0].docfield[32] "metalvalue"
+ilscript[0].docfield[33] "hiphopvalue"
+ilscript[0].docfield[34] "powermetalvalue"
+ilscript[0].docfield[35] "progvalue"
+ilscript[0].content[0] "clear_state | guard { input hiphopvalue | split \";\" | attribute hiphopvalue_arr; }"
+ilscript[0].content[1] "clear_state | guard { input metalvalue | split \";\" | attribute metalvalue_arr; }"
+ilscript[0].content[2] "clear_state | guard { input bgndata | tokenize normalize stem:\"SHORTEST\" | summary bgndata; }"
+ilscript[0].content[3] "clear_state | guard { input sales | summary sales | attribute sales; }"
+ilscript[0].content[4] "clear_state | guard { input pto | summary pto | attribute pto; }"
+ilscript[0].content[5] "clear_state | guard { input keys | tokenize normalize stem:\"SHORTEST\" | index keys; }"
+ilscript[0].content[6] "clear_state | guard { input mid | summary mid | attribute mid; }"
+ilscript[0].content[7] "clear_state | guard { input ew | tokenize normalize stem:\"SHORTEST\" | summary ew | index ew; }"
+ilscript[0].content[8] "clear_state | guard { input surl | summary surl; }"
+ilscript[0].content[9] "clear_state | guard { input userrate | summary userrate; }"
+ilscript[0].content[10] "clear_state | guard { input pid | summary pid; }"
+ilscript[0].content[11] "clear_state | guard { input weight | summary weight | attribute weight; }"
+ilscript[0].content[12] "clear_state | guard { input url | summary url; }"
+ilscript[0].content[13] "clear_state | guard { input isbn | summary isbn; }"
+ilscript[0].content[14] "clear_state | guard { input fmt | tokenize normalize stem:\"SHORTEST\" | summary fmt | index fmt; }"
+ilscript[0].content[15] "clear_state | guard { input albumid | summary albumid; }"
+ilscript[0].content[16] "clear_state | guard { input disp_song | summary disp_song; }"
+ilscript[0].content[17] "clear_state | guard { input song | tokenize normalize stem:\"SHORTEST\" | summary song | index song; }"
+ilscript[0].content[18] "clear_state | guard { input pfrom | summary pfrom; }"
+ilscript[0].content[19] "clear_state | guard { input bgnpfrom | summary bgnpfrom | attribute bgnpfrom; }"
+ilscript[0].content[20] "clear_state | guard { input categories | tokenize normalize stem:\"SHORTEST\" | summary categories | index categories; }"
+ilscript[0].content[21] "clear_state | guard { input data | summary data; }"
+ilscript[0].content[22] "clear_state | guard { input numreview | summary numreview; }"
+ilscript[0].content[23] "clear_state | guard { input bgnsellers | summary bgnsellers; }"
+ilscript[0].content[24] "clear_state | guard { input image | summary image; }"
+ilscript[0].content[25] "clear_state | guard { input artist | tokenize normalize stem:\"SHORTEST\" | summary artist | index artist; }"
+ilscript[0].content[26] "clear_state | guard { input artistspid | summary artistspid; }"
+ilscript[0].content[27] "clear_state | guard { input title | tokenize normalize stem:\"SHORTEST\" | summary title | index title; }"
+ilscript[0].content[28] "clear_state | guard { input newestedition | summary newestedition | attribute newestedition; }"
+ilscript[0].content[29] "clear_state | guard { input bgnpto | tokenize normalize stem:\"SHORTEST\" | summary bgnpto; }"
+ilscript[0].content[30] "clear_state | guard { input year | summary year | attribute year; }"
+ilscript[0].content[31] "clear_state | guard { input did | summary did | attribute did; }"
+ilscript[0].content[32] "clear_state | guard { input scorekey | summary scorekey; }"
+ilscript[0].content[33] "clear_state | guard { input cbid | summary cbid | attribute cbid; }"
+ilscript[0].content[34] "clear_state | guard { input metalvalue | summary metalvalue; }"
+ilscript[0].content[35] "clear_state | guard { input hiphopvalue | summary hiphopvalue; }"
+ilscript[0].content[36] "clear_state | guard { input powermetalvalue | tokenize normalize stem:\"SHORTEST\" | index powermetalvalue | summary powermetalvalue; }"
+ilscript[0].content[37] "clear_state | guard { input progvalue | tokenize normalize stem:\"SHORTEST\" | index progvalue | summary progvalue; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/music/index-info.cfg b/config-model/src/test/derived/music/index-info.cfg
new file mode 100644
index 00000000000..f7faae11ca8
--- /dev/null
+++ b/config-model/src/test/derived/music/index-info.cfg
@@ -0,0 +1,215 @@
+indexinfo[0].name "music"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "bgndata"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "sales"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "sales"
+indexinfo[0].command[4].command "attribute"
+indexinfo[0].command[5].indexname "sales"
+indexinfo[0].command[5].command "numerical"
+indexinfo[0].command[6].indexname "pto"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "pto"
+indexinfo[0].command[7].command "attribute"
+indexinfo[0].command[8].indexname "pto"
+indexinfo[0].command[8].command "numerical"
+indexinfo[0].command[9].indexname "keys"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[10].indexname "default"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "keys"
+indexinfo[0].command[11].command "lowercase"
+indexinfo[0].command[12].indexname "default"
+indexinfo[0].command[12].command "lowercase"
+indexinfo[0].command[13].indexname "keys"
+indexinfo[0].command[13].command "stem:SHORTEST"
+indexinfo[0].command[14].indexname "default"
+indexinfo[0].command[14].command "stem:SHORTEST"
+indexinfo[0].command[15].indexname "keys"
+indexinfo[0].command[15].command "normalize"
+indexinfo[0].command[16].indexname "default"
+indexinfo[0].command[16].command "normalize"
+indexinfo[0].command[17].indexname "mid"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "mid"
+indexinfo[0].command[18].command "attribute"
+indexinfo[0].command[19].indexname "mid"
+indexinfo[0].command[19].command "numerical"
+indexinfo[0].command[20].indexname "surl"
+indexinfo[0].command[20].command "index"
+indexinfo[0].command[21].indexname "userrate"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "userrate"
+indexinfo[0].command[22].command "numerical"
+indexinfo[0].command[23].indexname "pid"
+indexinfo[0].command[23].command "index"
+indexinfo[0].command[24].indexname "weight"
+indexinfo[0].command[24].command "index"
+indexinfo[0].command[25].indexname "weight"
+indexinfo[0].command[25].command "attribute"
+indexinfo[0].command[26].indexname "weight"
+indexinfo[0].command[26].command "numerical"
+indexinfo[0].command[27].indexname "url"
+indexinfo[0].command[27].command "index"
+indexinfo[0].command[28].indexname "isbn"
+indexinfo[0].command[28].command "index"
+indexinfo[0].command[29].indexname "fmt"
+indexinfo[0].command[29].command "index"
+indexinfo[0].command[30].indexname "fmt"
+indexinfo[0].command[30].command "lowercase"
+indexinfo[0].command[31].indexname "fmt"
+indexinfo[0].command[31].command "stem:SHORTEST"
+indexinfo[0].command[32].indexname "fmt"
+indexinfo[0].command[32].command "normalize"
+indexinfo[0].command[33].indexname "albumid"
+indexinfo[0].command[33].command "index"
+indexinfo[0].command[34].indexname "disp_song"
+indexinfo[0].command[34].command "index"
+indexinfo[0].command[35].indexname "song"
+indexinfo[0].command[35].command "index"
+indexinfo[0].command[36].indexname "song"
+indexinfo[0].command[36].command "lowercase"
+indexinfo[0].command[37].indexname "song"
+indexinfo[0].command[37].command "stem:SHORTEST"
+indexinfo[0].command[38].indexname "song"
+indexinfo[0].command[38].command "normalize"
+indexinfo[0].command[39].indexname "pfrom"
+indexinfo[0].command[39].command "index"
+indexinfo[0].command[40].indexname "pfrom"
+indexinfo[0].command[40].command "numerical"
+indexinfo[0].command[41].indexname "bgnpfrom"
+indexinfo[0].command[41].command "index"
+indexinfo[0].command[42].indexname "bgnpfrom"
+indexinfo[0].command[42].command "attribute"
+indexinfo[0].command[43].indexname "bgnpfrom"
+indexinfo[0].command[43].command "numerical"
+indexinfo[0].command[44].indexname "categories"
+indexinfo[0].command[44].command "index"
+indexinfo[0].command[45].indexname "categories"
+indexinfo[0].command[45].command "lowercase"
+indexinfo[0].command[46].indexname "categories"
+indexinfo[0].command[46].command "stem:SHORTEST"
+indexinfo[0].command[47].indexname "categories"
+indexinfo[0].command[47].command "normalize"
+indexinfo[0].command[48].indexname "data"
+indexinfo[0].command[48].command "index"
+indexinfo[0].command[49].indexname "numreview"
+indexinfo[0].command[49].command "index"
+indexinfo[0].command[50].indexname "numreview"
+indexinfo[0].command[50].command "numerical"
+indexinfo[0].command[51].indexname "bgnsellers"
+indexinfo[0].command[51].command "index"
+indexinfo[0].command[52].indexname "bgnsellers"
+indexinfo[0].command[52].command "numerical"
+indexinfo[0].command[53].indexname "image"
+indexinfo[0].command[53].command "index"
+indexinfo[0].command[54].indexname "artist"
+indexinfo[0].command[54].command "index"
+indexinfo[0].command[55].indexname "artist"
+indexinfo[0].command[55].command "lowercase"
+indexinfo[0].command[56].indexname "artist"
+indexinfo[0].command[56].command "stem:SHORTEST"
+indexinfo[0].command[57].indexname "artist"
+indexinfo[0].command[57].command "normalize"
+indexinfo[0].command[58].indexname "artistspid"
+indexinfo[0].command[58].command "index"
+indexinfo[0].command[59].indexname "title"
+indexinfo[0].command[59].command "index"
+indexinfo[0].command[60].indexname "title"
+indexinfo[0].command[60].command "lowercase"
+indexinfo[0].command[61].indexname "title"
+indexinfo[0].command[61].command "stem:SHORTEST"
+indexinfo[0].command[62].indexname "title"
+indexinfo[0].command[62].command "normalize"
+indexinfo[0].command[63].indexname "newestedition"
+indexinfo[0].command[63].command "index"
+indexinfo[0].command[64].indexname "newestedition"
+indexinfo[0].command[64].command "attribute"
+indexinfo[0].command[65].indexname "newestedition"
+indexinfo[0].command[65].command "numerical"
+indexinfo[0].command[66].indexname "bgnpto"
+indexinfo[0].command[66].command "index"
+indexinfo[0].command[67].indexname "year"
+indexinfo[0].command[67].command "index"
+indexinfo[0].command[68].indexname "year"
+indexinfo[0].command[68].command "attribute"
+indexinfo[0].command[69].indexname "year"
+indexinfo[0].command[69].command "numerical"
+indexinfo[0].command[70].indexname "did"
+indexinfo[0].command[70].command "index"
+indexinfo[0].command[71].indexname "did"
+indexinfo[0].command[71].command "attribute"
+indexinfo[0].command[72].indexname "did"
+indexinfo[0].command[72].command "numerical"
+indexinfo[0].command[73].indexname "scorekey"
+indexinfo[0].command[73].command "index"
+indexinfo[0].command[74].indexname "scorekey"
+indexinfo[0].command[74].command "numerical"
+indexinfo[0].command[75].indexname "cbid"
+indexinfo[0].command[75].command "index"
+indexinfo[0].command[76].indexname "cbid"
+indexinfo[0].command[76].command "attribute"
+indexinfo[0].command[77].indexname "cbid"
+indexinfo[0].command[77].command "numerical"
+indexinfo[0].command[78].indexname "metalvalue"
+indexinfo[0].command[78].command "index"
+indexinfo[0].command[79].indexname "hiphopvalue"
+indexinfo[0].command[79].command "index"
+indexinfo[0].command[80].indexname "powermetalvalue"
+indexinfo[0].command[80].command "index"
+indexinfo[0].command[81].indexname "powermetalvalue"
+indexinfo[0].command[81].command "lowercase"
+indexinfo[0].command[82].indexname "powermetalvalue"
+indexinfo[0].command[82].command "stem:SHORTEST"
+indexinfo[0].command[83].indexname "powermetalvalue"
+indexinfo[0].command[83].command "normalize"
+indexinfo[0].command[84].indexname "progvalue"
+indexinfo[0].command[84].command "index"
+indexinfo[0].command[85].indexname "progvalue"
+indexinfo[0].command[85].command "lowercase"
+indexinfo[0].command[86].indexname "progvalue"
+indexinfo[0].command[86].command "stem:SHORTEST"
+indexinfo[0].command[87].indexname "progvalue"
+indexinfo[0].command[87].command "normalize"
+indexinfo[0].command[88].indexname "hiphopvalue_arr"
+indexinfo[0].command[88].command "index"
+indexinfo[0].command[89].indexname "hiphopvalue_arr"
+indexinfo[0].command[89].command "multivalue"
+indexinfo[0].command[90].indexname "hiphopvalue_arr"
+indexinfo[0].command[90].command "attribute"
+indexinfo[0].command[91].indexname "hiphopvalue_arr"
+indexinfo[0].command[91].command "word"
+indexinfo[0].command[92].indexname "metalvalue_arr"
+indexinfo[0].command[92].command "index"
+indexinfo[0].command[93].indexname "metalvalue_arr"
+indexinfo[0].command[93].command "multivalue"
+indexinfo[0].command[94].indexname "metalvalue_arr"
+indexinfo[0].command[94].command "attribute"
+indexinfo[0].command[95].indexname "metalvalue_arr"
+indexinfo[0].command[95].command "word"
+indexinfo[0].command[96].indexname "rankfeatures"
+indexinfo[0].command[96].command "index"
+indexinfo[0].command[97].indexname "summaryfeatures"
+indexinfo[0].command[97].command "index"
+indexinfo[0].command[98].indexname "bgndata"
+indexinfo[0].command[98].command "dynteaser"
+indexinfo[0].command[29].indexname "ew"
+indexinfo[0].command[29].command "index"
+indexinfo[0].command[30].indexname "ew"
+indexinfo[0].command[30].command "lowercase"
+indexinfo[0].command[31].indexname "ew"
+indexinfo[0].command[31].command "stem:SHORTEST"
+indexinfo[0].command[32].indexname "ew"
+indexinfo[0].command[102].indexname "ew"
+indexinfo[0].command[102].command "highlight"
+indexinfo[0].command[32].command "normalize"
+indexinfo[0].command[100].indexname "song"
+indexinfo[0].command[100].command "dynteaser"
+indexinfo[0].command[101].indexname "bgnpto"
+indexinfo[0].command[101].command "dynteaser"
+indexinfo[0].command[102].indexname "bgnpto"
+indexinfo[0].command[102].command "highlight" \ No newline at end of file
diff --git a/config-model/src/test/derived/music/juniperrc.cfg b/config-model/src/test/derived/music/juniperrc.cfg
new file mode 100755
index 00000000000..b5d7438e081
--- /dev/null
+++ b/config-model/src/test/derived/music/juniperrc.cfg
@@ -0,0 +1,21 @@
+length 256
+max_matches 3
+min_length 128
+prefix true
+surround_max 128
+winsize 200
+winsize_fallback_multiplier 10.0
+max_match_candidates 1000
+stem_min_length 5
+stem_max_extend 3
+override[0].fieldname "ew"
+override[0].length 65536
+override[0].max_matches 1
+override[0].min_length 8192
+override[0].prefix true
+override[0].surround_max 65536
+override[0].winsize 200
+override[0].winsize_fallback_multiplier 10.0
+override[0].max_match_candidates 1000
+override[0].stem_min_length 5
+override[0].stem_max_extend 3 \ No newline at end of file
diff --git a/config-model/src/test/derived/music/music.sd b/config-model/src/test/derived/music/music.sd
new file mode 100644
index 00000000000..b1a2bfc385d
--- /dev/null
+++ b/config-model/src/test/derived/music/music.sd
@@ -0,0 +1,176 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document music {
+
+ field bgndata type string {
+ indexing: summary
+ summary: dynamic
+ }
+
+ field sales type int {
+ indexing: summary | attribute
+ }
+
+ field pto type int {
+ indexing: summary | attribute
+ }
+
+ field keys type string {
+ indexing: index
+ }
+
+ field mid type int {
+ indexing: summary | attribute
+ }
+
+ field ew type string {
+ indexing: summary | index
+ bolding: on
+ }
+
+ field surl type string {
+ indexing: summary
+ }
+
+ field userrate type int {
+ indexing: summary
+ }
+
+ field pid type string {
+ indexing: summary
+ }
+
+ field weight type float {
+ indexing: summary | attribute
+ }
+
+ field url type string {
+ indexing: summary
+ }
+
+ field isbn type string {
+ indexing: summary
+ }
+
+ field fmt type string {
+ indexing: summary | index
+ }
+
+ field albumid type string {
+ indexing: summary
+ }
+
+ field disp_song type string {
+ indexing: summary
+ }
+
+ field song type string {
+ indexing: summary | index
+ summary: dynamic
+ }
+
+ field pfrom type int {
+ indexing: summary
+ }
+
+ field bgnpfrom type float {
+ indexing: summary | attribute
+ }
+
+ field categories type string {
+ indexing: summary | index
+ }
+
+ field data type string {
+ indexing: summary
+ }
+
+ field numreview type int {
+ indexing: summary
+ }
+
+ field bgnsellers type int {
+ indexing: summary
+ }
+
+ field image type string {
+ indexing: summary
+ }
+
+ field artist type string {
+ indexing: summary | index
+ }
+
+ field artistspid type string {
+ indexing: summary
+ }
+
+ field title type string {
+ indexing: summary | index
+ }
+
+ field newestedition type int {
+ indexing: summary | attribute
+ }
+
+ field bgnpto type string {
+ indexing: summary
+ summary: dynamic
+ bolding: on
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ }
+
+ field did type int {
+ indexing: summary | attribute
+ }
+
+ field scorekey type int {
+ indexing: summary
+ }
+
+ field cbid type int {
+ indexing: summary | attribute
+ }
+
+ field metalvalue type string {
+ indexing: summary
+ }
+
+ field hiphopvalue type string {
+ indexing: summary
+ match {
+ token
+ }
+ }
+
+ field powermetalvalue type string {
+ indexing: index | summary
+ rank: filter
+ }
+
+ field progvalue type string {
+ indexing: index | summary
+ rank {
+ filter
+ }
+ }
+ }
+
+ field metalvalue_arr type array<string> {
+ indexing: input metalvalue | split ";" | attribute
+ }
+
+ field hiphopvalue_arr type array<string> {
+ indexing: input hiphopvalue | split ";" | attribute
+ }
+
+ fieldset default {
+ fields: keys, ew, artist, song, title
+ }
+
+}
+
diff --git a/config-model/src/test/derived/music/rank-profiles.cfg b/config-model/src/test/derived/music/rank-profiles.cfg
new file mode 100644
index 00000000000..e2452a670a0
--- /dev/null
+++ b/config-model/src/test/derived/music/rank-profiles.cfg
@@ -0,0 +1,34 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.powermetalvalue"
+rankprofile[0].fef.property[0].value "linear(0,0)"
+rankprofile[0].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.powermetalvalue"
+rankprofile[0].fef.property[1].value "linear(0,0)"
+rankprofile[0].fef.property[2].name "nativeProximity.proximityTable.powermetalvalue"
+rankprofile[0].fef.property[2].value "linear(0,0)"
+rankprofile[0].fef.property[3].name "nativeProximity.reverseProximityTable.powermetalvalue"
+rankprofile[0].fef.property[3].value "linear(0,0)"
+rankprofile[0].fef.property[4].name "nativeFieldMatch.firstOccurrenceTable.progvalue"
+rankprofile[0].fef.property[4].value "linear(0,0)"
+rankprofile[0].fef.property[5].name "nativeFieldMatch.occurrenceCountTable.progvalue"
+rankprofile[0].fef.property[5].value "linear(0,0)"
+rankprofile[0].fef.property[6].name "nativeProximity.proximityTable.progvalue"
+rankprofile[0].fef.property[6].value "linear(0,0)"
+rankprofile[0].fef.property[7].name "nativeProximity.reverseProximityTable.progvalue"
+rankprofile[0].fef.property[7].value "linear(0,0)"
+rankprofile[0].fef.property[9].name "vespa.isfilterfield.progvalue"
+rankprofile[0].fef.property[9].value "true"
+rankprofile[0].fef.property[10].name "vespa.isfilterfield.powermetalvalue"
+rankprofile[0].fef.property[10].value "true"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[1].fef.property[5].name "vespa.isfilterfield.progvalue"
+rankprofile[1].fef.property[5].value "true"
+rankprofile[1].fef.property[6].name "vespa.isfilterfield.powermetalvalue"
+rankprofile[1].fef.property[6].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/music/summary.cfg b/config-model/src/test/derived/music/summary.cfg
new file mode 100644
index 00000000000..0ddb8940482
--- /dev/null
+++ b/config-model/src/test/derived/music/summary.cfg
@@ -0,0 +1,103 @@
+defaultsummaryid 2086497905
+classes[0].id 2086497905
+classes[0].name "default"
+classes[0].fields[0].name "bgndata"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "sales"
+classes[0].fields[1].type "integer"
+classes[0].fields[2].name "pto"
+classes[0].fields[2].type "integer"
+classes[0].fields[3].name "mid"
+classes[0].fields[3].type "integer"
+classes[0].fields[4].name "ew"
+classes[0].fields[4].type "longstring"
+classes[0].fields[5].name "surl"
+classes[0].fields[5].type "longstring"
+classes[0].fields[6].name "userrate"
+classes[0].fields[6].type "integer"
+classes[0].fields[7].name "pid"
+classes[0].fields[7].type "longstring"
+classes[0].fields[8].name "weight"
+classes[0].fields[8].type "float"
+classes[0].fields[9].name "url"
+classes[0].fields[9].type "longstring"
+classes[0].fields[10].name "isbn"
+classes[0].fields[10].type "longstring"
+classes[0].fields[11].name "fmt"
+classes[0].fields[11].type "longstring"
+classes[0].fields[12].name "albumid"
+classes[0].fields[12].type "longstring"
+classes[0].fields[13].name "disp_song"
+classes[0].fields[13].type "longstring"
+classes[0].fields[14].name "song"
+classes[0].fields[14].type "longstring"
+classes[0].fields[15].name "pfrom"
+classes[0].fields[15].type "integer"
+classes[0].fields[16].name "bgnpfrom"
+classes[0].fields[16].type "float"
+classes[0].fields[17].name "categories"
+classes[0].fields[17].type "longstring"
+classes[0].fields[18].name "data"
+classes[0].fields[18].type "longstring"
+classes[0].fields[19].name "numreview"
+classes[0].fields[19].type "integer"
+classes[0].fields[20].name "bgnsellers"
+classes[0].fields[20].type "integer"
+classes[0].fields[21].name "image"
+classes[0].fields[21].type "longstring"
+classes[0].fields[22].name "artist"
+classes[0].fields[22].type "longstring"
+classes[0].fields[23].name "artistspid"
+classes[0].fields[23].type "longstring"
+classes[0].fields[24].name "title"
+classes[0].fields[24].type "longstring"
+classes[0].fields[25].name "newestedition"
+classes[0].fields[25].type "integer"
+classes[0].fields[26].name "bgnpto"
+classes[0].fields[26].type "longstring"
+classes[0].fields[27].name "year"
+classes[0].fields[27].type "integer"
+classes[0].fields[28].name "did"
+classes[0].fields[28].type "integer"
+classes[0].fields[29].name "scorekey"
+classes[0].fields[29].type "integer"
+classes[0].fields[30].name "cbid"
+classes[0].fields[30].type "integer"
+classes[0].fields[31].name "metalvalue"
+classes[0].fields[31].type "longstring"
+classes[0].fields[32].name "hiphopvalue"
+classes[0].fields[32].type "longstring"
+classes[0].fields[33].name "powermetalvalue"
+classes[0].fields[33].type "longstring"
+classes[0].fields[34].name "progvalue"
+classes[0].fields[34].type "longstring"
+classes[0].fields[35].name "rankfeatures"
+classes[0].fields[35].type "featuredata"
+classes[0].fields[36].name "summaryfeatures"
+classes[0].fields[36].type "featuredata"
+classes[0].fields[37].name "documentid"
+classes[0].fields[37].type "longstring"
+classes[1].id 2060710706
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "sales"
+classes[1].fields[0].type "integer"
+classes[1].fields[1].name "pto"
+classes[1].fields[1].type "integer"
+classes[1].fields[2].name "mid"
+classes[1].fields[2].type "integer"
+classes[1].fields[3].name "weight"
+classes[1].fields[3].type "float"
+classes[1].fields[4].name "bgnpfrom"
+classes[1].fields[4].type "float"
+classes[1].fields[5].name "newestedition"
+classes[1].fields[5].type "integer"
+classes[1].fields[6].name "year"
+classes[1].fields[6].type "integer"
+classes[1].fields[7].name "did"
+classes[1].fields[7].type "integer"
+classes[1].fields[8].name "cbid"
+classes[1].fields[8].type "integer"
+classes[1].fields[9].name "rankfeatures"
+classes[1].fields[9].type "featuredata"
+classes[1].fields[10].name "summaryfeatures"
+classes[1].fields[10].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/music/summarymap.cfg b/config-model/src/test/derived/music/summarymap.cfg
new file mode 100644
index 00000000000..f86dd8ec98f
--- /dev/null
+++ b/config-model/src/test/derived/music/summarymap.cfg
@@ -0,0 +1,46 @@
+defaultoutputclass -1
+override[0].field "bgndata"
+override[0].command "dynamicteaser"
+override[0].arguments "bgndata"
+override[1].field "sales"
+override[1].command "attribute"
+override[1].arguments "sales"
+override[2].field "pto"
+override[2].command "attribute"
+override[2].arguments "pto"
+override[3].field "mid"
+override[3].command "attribute"
+override[3].arguments "mid"
+override[4].field "ew"
+override[4].command "dynamicteaser"
+override[4].arguments "ew"
+override[5].field "weight"
+override[5].command "attribute"
+override[5].arguments "weight"
+override[6].field "song"
+override[6].command "dynamicteaser"
+override[6].arguments "song"
+override[7].field "bgnpfrom"
+override[7].command "attribute"
+override[7].arguments "bgnpfrom"
+override[8].field "newestedition"
+override[8].command "attribute"
+override[8].arguments "newestedition"
+override[9].field "bgnpto"
+override[9].command "dynamicteaser"
+override[9].arguments "bgnpto"
+override[10].field "year"
+override[10].command "attribute"
+override[10].arguments "year"
+override[11].field "did"
+override[11].command "attribute"
+override[11].arguments "did"
+override[12].field "cbid"
+override[12].command "attribute"
+override[12].arguments "cbid"
+override[13].field "rankfeatures"
+override[13].command "rankfeatures"
+override[13].arguments ""
+override[14].field "summaryfeatures"
+override[14].command "summaryfeatures"
+override[14].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/music3/music3.sd b/config-model/src/test/derived/music3/music3.sd
new file mode 100644
index 00000000000..e1c78406234
--- /dev/null
+++ b/config-model/src/test/derived/music3/music3.sd
@@ -0,0 +1,70 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music3 {
+
+ document music3 {
+
+ field title type string {
+ indexing: summary | index
+ # index-to: title, default
+ rank-type: about
+
+ header
+ }
+
+ field artist type string {
+ indexing: summary | attribute | index
+ # index-to: artist, default
+ rank-type:about
+
+ header
+ }
+
+ field year type int {
+ indexing: summary | attribute
+
+ header
+ }
+
+ # Increase rank score of popular documents regardless of query
+ field popularity type int {
+ indexing: summary | attribute
+
+ header
+ }
+
+ field url type uri {
+ indexing: summary | index
+
+ header
+ }
+
+ }
+
+ rank-profile other {
+
+ first-phase {
+ expression: nativeRank
+ }
+
+ second-phase {
+ expression: 0.5 * 0.5 * (0.1 * attribute(popularity) + fieldMatch(artist)) + 0.3 * fieldMatch(title)
+ rerank-count:200
+ }
+ }
+
+ rank-profile default {
+
+ first-phase {
+ expression: nativeRank
+ }
+
+ second-phase {
+ expression {
+ 0.5 * 0.5 * (0.1 * attribute(popularity) + fieldMatch(artist))
+ + 0.3 * fieldMatch(title)
+ }
+ rerank-count:200
+ }
+ }
+
+}
diff --git a/config-model/src/test/derived/newrank/attributes.cfg b/config-model/src/test/derived/newrank/attributes.cfg
new file mode 100644
index 00000000000..536e07ac13c
--- /dev/null
+++ b/config-model/src/test/derived/newrank/attributes.cfg
@@ -0,0 +1,190 @@
+attribute[0].name "sales"
+attribute[0].datatype INT32
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "pto"
+attribute[1].datatype INT32
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "mid"
+attribute[2].datatype INT32
+attribute[2].collectiontype SINGLE
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending true
+attribute[2].sortfunction UCA
+attribute[2].sortstrength PRIMARY
+attribute[2].sortlocale ""
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype ""
+attribute[3].name "weight"
+attribute[3].datatype FLOAT
+attribute[3].collectiontype SINGLE
+attribute[3].removeifzero false
+attribute[3].createifnonexistent false
+attribute[3].fastsearch false
+attribute[3].huge false
+attribute[3].sortascending true
+attribute[3].sortfunction UCA
+attribute[3].sortstrength PRIMARY
+attribute[3].sortlocale ""
+attribute[3].enablebitvectors false
+attribute[3].enableonlybitvector false
+attribute[3].fastaccess false
+attribute[3].arity 8
+attribute[3].lowerbound -9223372036854775808
+attribute[3].upperbound 9223372036854775807
+attribute[3].densepostinglistthreshold 0.4
+attribute[3].tensortype ""
+attribute[4].name "bgnpfrom"
+attribute[4].datatype FLOAT
+attribute[4].collectiontype SINGLE
+attribute[4].removeifzero false
+attribute[4].createifnonexistent false
+attribute[4].fastsearch false
+attribute[4].huge false
+attribute[4].sortascending true
+attribute[4].sortfunction UCA
+attribute[4].sortstrength PRIMARY
+attribute[4].sortlocale ""
+attribute[4].enablebitvectors false
+attribute[4].enableonlybitvector false
+attribute[4].fastaccess false
+attribute[4].arity 8
+attribute[4].lowerbound -9223372036854775808
+attribute[4].upperbound 9223372036854775807
+attribute[4].densepostinglistthreshold 0.4
+attribute[4].tensortype ""
+attribute[5].name "newestedition"
+attribute[5].datatype INT32
+attribute[5].collectiontype SINGLE
+attribute[5].removeifzero false
+attribute[5].createifnonexistent false
+attribute[5].fastsearch false
+attribute[5].huge false
+attribute[5].sortascending true
+attribute[5].sortfunction UCA
+attribute[5].sortstrength PRIMARY
+attribute[5].sortlocale ""
+attribute[5].enablebitvectors false
+attribute[5].enableonlybitvector false
+attribute[5].fastaccess false
+attribute[5].arity 8
+attribute[5].lowerbound -9223372036854775808
+attribute[5].upperbound 9223372036854775807
+attribute[5].densepostinglistthreshold 0.4
+attribute[5].tensortype ""
+attribute[6].name "year"
+attribute[6].datatype INT32
+attribute[6].collectiontype SINGLE
+attribute[6].removeifzero false
+attribute[6].createifnonexistent false
+attribute[6].fastsearch false
+attribute[6].huge false
+attribute[6].sortascending true
+attribute[6].sortfunction UCA
+attribute[6].sortstrength PRIMARY
+attribute[6].sortlocale ""
+attribute[6].enablebitvectors false
+attribute[6].enableonlybitvector false
+attribute[6].fastaccess false
+attribute[6].arity 8
+attribute[6].lowerbound -9223372036854775808
+attribute[6].upperbound 9223372036854775807
+attribute[6].densepostinglistthreshold 0.4
+attribute[6].tensortype ""
+attribute[7].name "did"
+attribute[7].datatype INT32
+attribute[7].collectiontype SINGLE
+attribute[7].removeifzero false
+attribute[7].createifnonexistent false
+attribute[7].fastsearch false
+attribute[7].huge false
+attribute[7].sortascending true
+attribute[7].sortfunction UCA
+attribute[7].sortstrength PRIMARY
+attribute[7].sortlocale ""
+attribute[7].enablebitvectors false
+attribute[7].enableonlybitvector false
+attribute[7].fastaccess false
+attribute[7].arity 8
+attribute[7].lowerbound -9223372036854775808
+attribute[7].upperbound 9223372036854775807
+attribute[7].densepostinglistthreshold 0.4
+attribute[7].tensortype ""
+attribute[8].name "scorekey"
+attribute[8].datatype INT32
+attribute[8].collectiontype SINGLE
+attribute[8].removeifzero false
+attribute[8].createifnonexistent false
+attribute[8].fastsearch false
+attribute[8].huge false
+attribute[8].sortascending true
+attribute[8].sortfunction UCA
+attribute[8].sortstrength PRIMARY
+attribute[8].sortlocale ""
+attribute[8].enablebitvectors false
+attribute[8].enableonlybitvector false
+attribute[8].fastaccess false
+attribute[8].arity 8
+attribute[8].lowerbound -9223372036854775808
+attribute[8].upperbound 9223372036854775807
+attribute[8].densepostinglistthreshold 0.4
+attribute[8].tensortype ""
+attribute[9].name "cbid"
+attribute[9].datatype INT32
+attribute[9].collectiontype SINGLE
+attribute[9].removeifzero false
+attribute[9].createifnonexistent false
+attribute[9].fastsearch false
+attribute[9].huge false
+attribute[9].sortascending true
+attribute[9].sortfunction UCA
+attribute[9].sortstrength PRIMARY
+attribute[9].sortlocale ""
+attribute[9].enablebitvectors false
+attribute[9].enableonlybitvector false
+attribute[9].fastaccess false
+attribute[9].arity 8
+attribute[9].lowerbound -9223372036854775808
+attribute[9].upperbound 9223372036854775807
+attribute[9].densepostinglistthreshold 0.4
+attribute[9].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/newrank/defs/attributes.def b/config-model/src/test/derived/newrank/defs/attributes.def
new file mode 100644
index 00000000000..bb3a0df6299
--- /dev/null
+++ b/config-model/src/test/derived/newrank/defs/attributes.def
@@ -0,0 +1,7 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+attribute[].name string
+attribute[].datatype string
+attribute[].multivalue bool default=false
+attribute[].sortsigned bool default=true
+attribute[].disableprep bool default=false
diff --git a/config-model/src/test/derived/newrank/defs/documentmanager.def b/config-model/src/test/derived/newrank/defs/documentmanager.def
new file mode 100644
index 00000000000..a310e9f13c8
--- /dev/null
+++ b/config-model/src/test/derived/newrank/defs/documentmanager.def
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=document.config
+datatype[].id int
+datatype[].arraytype[].datatype int
+documenttype[].name string
+documenttype[].version int
+documenttype[].inherits[].name string
+documenttype[].inherits[].version int
+documenttype[].field[].name string
+documenttype[].field[].id int
+documenttype[].field[].header bool
+documenttype[].field[].datatype int
diff --git a/config-model/src/test/derived/newrank/defs/extra.def b/config-model/src/test/derived/newrank/defs/extra.def
new file mode 100644
index 00000000000..cc03f1b39d3
--- /dev/null
+++ b/config-model/src/test/derived/newrank/defs/extra.def
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=test
+file[].name string
+file[].content string
diff --git a/config-model/src/test/derived/newrank/defs/ilscripts.def b/config-model/src/test/derived/newrank/defs/ilscripts.def
new file mode 100644
index 00000000000..d999742fa3c
--- /dev/null
+++ b/config-model/src/test/derived/newrank/defs/ilscripts.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.configdefinition
+ilscript[].name string
+ilscript[].doctype string
+ilscript[].content[] string
diff --git a/config-model/src/test/derived/newrank/defs/rank-profiles.def b/config-model/src/test/derived/newrank/defs/rank-profiles.def
new file mode 100644
index 00000000000..0d7cf27ff06
--- /dev/null
+++ b/config-model/src/test/derived/newrank/defs/rank-profiles.def
@@ -0,0 +1,344 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+## name of this rank profile. maps to table index for internal use.
+rankprofile[].name string
+
+## the name of a generic property available to the feature execution framework and feature plugins
+rankprofile[].fef.property[].name string
+
+## the value of a generic property available to feature plugins
+rankprofile[].fef.property[].value string
+
+## the catalog name overrides apply to
+rankprofile[].catalog[].name string
+
+## Boost value for AND queries in this catalog.
+rankprofile[].catalog[].andboost int default=0
+
+## Boost value for OR queries in this catalog.
+rankprofile[].catalog[].orboost int default=0
+
+## Boost value for ANY queries in this catalog.
+rankprofile[].catalog[].anyboost int default=0
+
+## Boost value for NEAR queries in catalog.
+rankprofile[].catalog[].nearboost int default=0
+
+## Boost value for ORDEREDNEAR queries in this catalog.
+rankprofile[].catalog[].orderednearboost int default=0
+
+## Boost value for phrase queries in this catalog.
+rankprofile[].catalog[].phraseboost int default=0
+
+## Boost value for all queries in catalog.
+rankprofile[].catalog[].rankboost int default=0
+
+## If true, the context boost is the max value of
+## the individual contextboosts.
+## When false, the context boost when a term is in
+## several contexts is the sum of the individual contextboosts.
+rankprofile[].catalog[].bestcontextboostonly bool default=false
+
+
+## If true, then use extnumoccboost only when calculating rank values.
+## Also, do not normalize the extnumoccboost value with
+## global term frequency. Default value is false.
+rankprofile[].catalog[].extnumoccboostonly bool default=false
+
+## If yes, then use extnumoccboost only when calculating rank values.
+## Also, do not normalize the extnumoccboost value with
+## global term frequency. Default value is no.
+rankprofile[].catalog[].numoccandextnumoccboostonly bool default=false
+
+## If yes, then use bitvectors when possible.
+## Default value is false.
+rankprofile[].catalog[].preferbitvector bool default=false
+
+## Load extnumoccboost for this catalog from the named file.
+## extnumoccboost specifies boost values due to the number of
+## occurences of a term that are external to the document. If
+## "NULL" is given as file name, then all extnumoccboost values
+## will be set to 0.
+rankprofile[].catalog[].extnumoccboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load numoccboost for this catalog from the named file.
+## numoccboost specifies boost values due to the number of occurences in
+## a document. If "NULL" is given as file name, then all numoccboost
+## values will be set to 0.
+rankprofile[].catalog[].numoccboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load firstoccboost for catalog from the file named.
+## firstoccboost specifies boost values due to the position of the
+## first occurence in a document. If "NULL" is given as file name,
+## then all firstoccboost values will be set to 0.
+rankprofile[].catalog[].firstoccboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+
+## Load firstoccproximityboost for this catalog from the file named.
+## firstoccproximity boost specifies boost values due to the correlation between
+## positions of the first occurence in a document for two and two words.
+##
+## If "NULL" is given as file name, then all
+## firstoccproximityboost values will be set to 0. If otherwise set,
+## should be the name of a file to load into the table. The file
+## should have 256 lines each containing a single integer.
+##
+## There are 256 elements in the table, handling forward distances from 1.
+## The corresponding firstoccrevproximityboost table is used
+## to handle closeness in reverse order.
+##
+## The last array index specifies the proximity table set. During
+## evaluation, the bigram proximity weight supplied by the query segmenter
+## specifies which proximity table set to use, with a fallback to set 0
+## when no information is available.
+rankprofile[].catalog[].firstoccproximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load firstoccrevproximityboost table for this catalog from the named file.
+## Specifies boost values due to the correlation between positions
+## of the first occurence in a document for two and two words when
+## the second word in the query comes first in the document.
+## See also firstoccproximityboost above.
+rankprofile[].catalog[].firstoccrevproximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load proximityboost for this catalog from the named file.
+## proximity boost specifies boost values due to the correlation between
+## positions of the occurences in a document for two and two words.
+## See also firstoccproximityboost above.
+rankprofile[].catalog[].proximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load revproximityboost for this catalog from the named file.
+## revproximity boost specifies boost values due to the correlation between
+## positions of the occurences in a document for two and two words.
+## See also firstoccproximityboost above.
+rankprofile[].catalog[].revproximityboost[].table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## Load divtable for this catalog from the named file.
+## Rank values for a query term are divided by the entry
+## in divtable indexed by log2 of term frequence.
+## The file should contain ?? lines each with a single integer.
+rankprofile[].catalog[].divtable string default=""
+
+## The name of a context in this catalog to specify boosts for.
+rankprofile[].catalog[].context[].name string
+
+## Boost occurrences in this context with the given value.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].contextboost int default=0
+
+## Boost pair of occurrences in this context with
+## the given value when evaluating 2 words from same catalog in
+## parallell.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].commoncontextboost.pair int default=0
+
+## Boost triple of occurrences in this context with
+## the given value when evaluating 3 words from same catalog in
+## parallell.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].commoncontextboost.triple int default=0
+
+## Boost quad of occurrences in this context with
+## the given value when evaluating 4 words from same catalog in
+## parallell.
+## XXX -1 uses default (???) from somewhere(TM).
+rankprofile[].catalog[].context[].commoncontextboost.quad int default=0
+
+
+## The name of the attribute
+rankprofile[].attribute[].name string
+
+## Boost value for queries that hit in this attribute
+rankprofile[].attribute[].attributecontextboost int default=0
+
+## Load weightboost for this attribute from the named file.
+## weightboost specifies boost values due to the weight (weighted set)
+## or number of occurences (single, array) in an attribute.
+## If "NULL" is given as file name, then all weightboost values will be set to 0.
+rankprofile[].attribute[].weightboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+
+## Load static rank values from the given staticrank docattr vector.
+## Must be specified in index.cf as a staticrankfile.
+rankprofile[].staticrankfile string default=""
+
+## Multiply static rank values with given value when calculating total
+## rank value.
+rankprofile[].staticcoefficient int default=1
+
+## If false then use only static ranking when sorting result hits.
+## Default is true.
+rankprofile[].dynamicranking bool default=true
+
+## If dynamic ranking is turned off, then ascending will sort the
+## result hits with lowest static rank values first, while
+## descending will sort with highest static rank values
+## first. Default is descending. This keyword has no effect if
+## dynamic ranking is on.
+rankprofile[].staticranksortorder string default="descending"
+
+## Load static rank mapping from the file named table. The static
+## rank mapping maps each 8-bit static rank value into a 32-bit static
+## rank value. This option may only be used with 8-bit static rank files.
+rankprofile[].staticrankmap string default=""
+
+## If set to "true", total rank will be reduced when dynamic rank is less than
+## 25% of static rank, to suppress irrelevant hits from popular sites.
+## If set to "false", total rank is not reduced.
+rankprofile[].clampstaticrank bool default=false
+
+## Load document datetime values used for freshness boost calculation from
+## this file. The values must be coded as minutes since
+## 1900-01-01T00:00Z. The value 0 has the special meaning
+## "no datetime value exists".
+rankprofile[].freshnessboost.file string default=""
+
+## Load freshnessboost lookup-table values from the file named
+## table instead of using built-in default values. The file must
+## contain 32 white-space separated non-negative integers.
+rankprofile[].freshnessboost.table string default="/home/vespa/conf/vespa/search/ranktables/constant-0000"
+
+## When calculating the freshness boost value multiply difference between
+## current datetime and document datetime with timeoffset before taking
+## the base-2 logarithm. Default value is 1. Max value is 31.
+rankprofile[].freshnessboost.timeoffset int default=1
+
+## If a document has datetime value 0, then use defaultboostvalue
+## as freshness boost value instead of doing table lookup. The default
+## default value is 0 (no boost).
+rankprofile[].freshnessboost.defaultboostvalue int default=0
+
+## Multiply freshness boost value with coefficient when calculating
+## total freshness boost value. If coefficient 0 is used, no freshness
+## boost value will be computed or added. Default value is 0.
+rankprofile[].freshnessboost.coefficient int default=0
+
+## boost table files for distance ranking, 1 dimension.
+## The tables have 465 elements each, where slots 0..15 represents
+## distances 0..15 while the remaining slots represents distance
+## (16 + (slot & 15)) << ((slot >> 4) - 1). Linear interpolation is
+## used for distances "between" table slots.
+##
+## If "NULL" is given as the file name then all 1D distance boost values
+## for that table will be set to 0.
+rankprofile[].distance1dboosttable[].table string
+
+## boost table files for distance ranking, 2 dimensions.
+## The tables have 977 elements each, where slots 0..15 represents
+## square of distance being 0..15 while the remaining slots represents
+## square of distance distance being
+## (16 + (slot & 15)) << ((slot >> 4) - 1). Linear interpolation is
+## used for distances "between" table slots.
+##
+## If "NULL" is given as the file name then all 2D distance boost values
+## for that table will be set to 0.
+rankprofile[].distance2dboosttable[].table string
+
+## The lowest possible size of a ranked result. This is the lower ramp
+## of the percentage specified in the binsize variable. The default is
+## specified in fsearchrc.
+rankprofile[].binlow int default=-1
+
+## The high limit of the ranked result bin. If the percentage of the
+## resultset specified in binsize is higher than this limit, this will be
+## the max size. The default is specified in fsearchrc.
+rankprofile[].binhigh int default=-1
+
+## The size of the ranked results as a percentage of the total result
+## set size. The percentage can be ramped off with the binlow and binhigh
+## variables. The default is specified in fsearchrc.
+rankprofile[].binsize double default=-1
+
+## Minimum value for maximum value of number of 'posocc' entries for a word.
+## The default is specified in fsearchrc.
+rankprofile[].posbinlow int default=-1
+
+## Maximum value for maximum value of number of 'posocc' entries for a word.
+## The default is specified in fsearchrc.
+rankprofile[].posbinhigh int default=-1
+
+## The maximum value for number of 'posocc' entries for a word, specified
+## as a percentage of the number of documents in the index. If more
+## entries are needed for evaluation, posocc entries are not used for that
+## word and evaluation will be performed without full proximity support.
+## The percentage can be ramped off with the posbinlow and posbinhigh
+## variables. The default is specified in fsearchrc.
+rankprofile[].posbinsize int default=-1
+
+## Boost value that is added to the relevance score of hits from superior
+## searches (searches where recall is sacrificed for better
+## precision). The rank cutoff feature will not be affected by this
+## feature (rank cutoff is applied before the superior boost).
+rankprofile[].superiorboost int default=0
+
+## Name of rank profile to be used when running superior searches
+## (searches where recall is sacrificed for better precision). If not
+## set, the current ranking profile will be used.
+##
+## If a profile for a superior search has this set then a superior^2
+## search exist (with more recall sacrificed than for superior searches)
+## and search behavior is slightly changed:
+##
+## If the search node has been asked to perform a superior search then an
+## internal double fallthrough with (superior, superior^2) search is
+## performed. If the search node has been asked to perform an internal
+## double fallthrough then a triple fallthrough with (normal, superior,
+## superior^2) is performed.
+rankprofile[].superiorname string default=""
+
+## After all other rank calculations, the rank value is tuned according
+## to the tunefactor and tunebias values. The rank value is modified
+## as follows: new_rank = old_rank * tunefactor + tunebias.
+rankprofile[].tunefactor double default=1.0
+
+## After all other rank calculations, the rank value is tuned according
+## to the tunefactor and tunebias values. The rank value is modified
+## as follows: new_rank = old_rank * tunefactor + tunebias.
+rankprofile[].tunebias int default=0
+
+## A lower limit for the rankvalue of the results returned from the
+## search node. If rankcutoff.advanced is set to "true", determines
+## the constant value used in the internal advanced rank cutoff
+## calculations. This roughly reflects the expected rank contribution
+## of one good term.
+## The rankcutoff.val value and the rankcutoff.advanced parameter
+## may be used if you only want hits with a minimum relevancy to show
+## up in the resultset.
+## A value below zero means no rankcutoff is done.
+rankprofile[].rankcutoff.val int default=-1
+
+## When rankcutoff.val is in use, this flag controls whether to use
+## an internal calculation is used for determining the rank cutoff
+## value. If "false", use rankcutoff.val as a direct lower limit.
+rankprofile[].rankcutoff.advanced bool default=false
+
+## If set to "ON", use of posocc files is enabled, except when
+## "forceemptyposoccs" is set in fsearchrc or posocc files doesn't exist.
+## If set to "OFF", use of posocc files is disabled.
+## If "NOTSET" the fsearchrc "proximity" parameter is used instead.
+rankprofile[].proximity.full.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## If set to "ON", use of firstoccproximity is enabled.
+## If set to "OFF", use of firstoccproximity is disabled.
+## When NOTSET use the firstoccproximity value in fsearchrc configuration.
+rankprofile[].proximity.firstocc.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## If set to "ON", use of proximity (cf. proximity and firstoccproximity)
+## will affect phrases in addition to single words.
+## If set to "OFF", proximity is never used for phrases.
+## When NOTSET use the phraseproximity value in fsearchrc configuration.
+rankprofile[].proximity.phrase.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## Selects behavior when proximity can be used for two words but not three
+## words while firstoccproximity can be used for three words.
+## If set to "ON", then use proximity for two words.
+## If set to "OFF", then use firstoccproximity for three words.
+## When NOTSET use the proximitypairbeforefirstoccproximitytriple value
+## in fsearchrc configuration.
+rankprofile[].proximity.pairbeforefirstocctriple.enable enum { OFF, ON, NOTSET } default=NOTSET
+
+## Selects behavior when proximity can be used for three words but not four
+## words while firstoccproximity can be used for four words.
+## If set to "ON", then use proximity for three words.
+## If set to "OFF", then use firstoccproximity for four words.
+## When NOTSET use the proximitytriplebeforefirstoccproximityquad value
+rankprofile[].proximity.triplebeforefirstoccquad.enable enum { OFF, ON, NOTSET } default=NOTSET
diff --git a/config-model/src/test/derived/newrank/defs/summarymap.def b/config-model/src/test/derived/newrank/defs/summarymap.def
new file mode 100644
index 00000000000..31b8fc44b66
--- /dev/null
+++ b/config-model/src/test/derived/newrank/defs/summarymap.def
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=vespa.config.search
+## The default output summary class id, -1 to use the stored summary class id
+defaultoutputclass int default=-1
+
+## The name of the summary field which is overridden with a dynamic value
+override[].field string
+
+## The overriding command, one of:
+## staticrank, dynamicteaser, dynamicteasermetric, label, ranklog, empty, copy
+override[].command string
+
+## Space-seaparated arguments, dedendent on the given command
+override[].arguments string default=""
diff --git a/config-model/src/test/derived/newrank/ilscripts.cfg b/config-model/src/test/derived/newrank/ilscripts.cfg
new file mode 100644
index 00000000000..8a252be4dd0
--- /dev/null
+++ b/config-model/src/test/derived/newrank/ilscripts.cfg
@@ -0,0 +1,66 @@
+maxtermoccurrences 100
+ilscript[0].doctype "newrank"
+ilscript[0].docfield[0] "bgndata"
+ilscript[0].docfield[1] "sales"
+ilscript[0].docfield[2] "pto"
+ilscript[0].docfield[3] "keys"
+ilscript[0].docfield[4] "mid"
+ilscript[0].docfield[5] "ew"
+ilscript[0].docfield[6] "surl"
+ilscript[0].docfield[7] "userrate"
+ilscript[0].docfield[8] "pid"
+ilscript[0].docfield[9] "weight"
+ilscript[0].docfield[10] "url"
+ilscript[0].docfield[11] "isbn"
+ilscript[0].docfield[12] "fmt"
+ilscript[0].docfield[13] "albumid"
+ilscript[0].docfield[14] "disp_song"
+ilscript[0].docfield[15] "song"
+ilscript[0].docfield[16] "pfrom"
+ilscript[0].docfield[17] "bgnpfrom"
+ilscript[0].docfield[18] "categories"
+ilscript[0].docfield[19] "data"
+ilscript[0].docfield[20] "numreview"
+ilscript[0].docfield[21] "bgnsellers"
+ilscript[0].docfield[22] "image"
+ilscript[0].docfield[23] "artist"
+ilscript[0].docfield[24] "artistspid"
+ilscript[0].docfield[25] "title"
+ilscript[0].docfield[26] "newestedition"
+ilscript[0].docfield[27] "bgnpto"
+ilscript[0].docfield[28] "year"
+ilscript[0].docfield[29] "did"
+ilscript[0].docfield[30] "scorekey"
+ilscript[0].docfield[31] "cbid"
+ilscript[0].content[0] "clear_state | guard { input bgndata | tokenize normalize stem:\"SHORTEST\" | summary bgndata; }"
+ilscript[0].content[1] "clear_state | guard { input sales | summary sales | attribute sales; }"
+ilscript[0].content[2] "clear_state | guard { input pto | summary pto | attribute pto; }"
+ilscript[0].content[3] "clear_state | guard { input keys | tokenize normalize stem:\"SHORTEST\" | index keys; }"
+ilscript[0].content[4] "clear_state | guard { input mid | summary mid | attribute mid; }"
+ilscript[0].content[5] "clear_state | guard { input ew | tokenize normalize stem:\"SHORTEST\" | summary ew | index ew; }"
+ilscript[0].content[6] "clear_state | guard { input surl | summary surl; }"
+ilscript[0].content[7] "clear_state | guard { input userrate | summary userrate; }"
+ilscript[0].content[8] "clear_state | guard { input pid | summary pid; }"
+ilscript[0].content[9] "clear_state | guard { input weight | summary weight | attribute weight; }"
+ilscript[0].content[10] "clear_state | guard { input url | summary url; }"
+ilscript[0].content[11] "clear_state | guard { input isbn | summary isbn; }"
+ilscript[0].content[12] "clear_state | guard { input fmt | tokenize normalize stem:\"SHORTEST\" | summary fmt | index fmt; }"
+ilscript[0].content[13] "clear_state | guard { input albumid | summary albumid; }"
+ilscript[0].content[14] "clear_state | guard { input disp_song | summary disp_song; }"
+ilscript[0].content[15] "clear_state | guard { input song | tokenize normalize stem:\"SHORTEST\" | summary song | index song; }"
+ilscript[0].content[16] "clear_state | guard { input pfrom | summary pfrom; }"
+ilscript[0].content[17] "clear_state | guard { input bgnpfrom | summary bgnpfrom | attribute bgnpfrom; }"
+ilscript[0].content[18] "clear_state | guard { input categories | tokenize normalize stem:\"SHORTEST\" | summary categories | index categories; }"
+ilscript[0].content[19] "clear_state | guard { input data | summary data; }"
+ilscript[0].content[20] "clear_state | guard { input numreview | summary numreview; }"
+ilscript[0].content[21] "clear_state | guard { input bgnsellers | summary bgnsellers; }"
+ilscript[0].content[22] "clear_state | guard { input image | summary image; }"
+ilscript[0].content[23] "clear_state | guard { input artist | tokenize normalize stem:\"SHORTEST\" | summary artist | index artist; }"
+ilscript[0].content[24] "clear_state | guard { input artistspid | summary artistspid; }"
+ilscript[0].content[25] "clear_state | guard { input title | tokenize normalize stem:\"SHORTEST\" | summary title | index title; }"
+ilscript[0].content[26] "clear_state | guard { input newestedition | summary newestedition | attribute newestedition; }"
+ilscript[0].content[27] "clear_state | guard { input bgnpto | tokenize normalize stem:\"SHORTEST\" | summary bgnpto; }"
+ilscript[0].content[28] "clear_state | guard { input year | summary year | attribute year; }"
+ilscript[0].content[29] "clear_state | guard { input did | summary did | attribute did; }"
+ilscript[0].content[30] "clear_state | guard { input scorekey | summary scorekey | attribute scorekey; }"
+ilscript[0].content[31] "clear_state | guard { input cbid | summary cbid | attribute cbid; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/newrank/index-info.cfg b/config-model/src/test/derived/newrank/index-info.cfg
new file mode 100644
index 00000000000..a74a7a3de08
--- /dev/null
+++ b/config-model/src/test/derived/newrank/index-info.cfg
@@ -0,0 +1,181 @@
+indexinfo[0].name "newrank"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "bgndata"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "sales"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "sales"
+indexinfo[0].command[4].command "attribute"
+indexinfo[0].command[5].indexname "sales"
+indexinfo[0].command[5].command "numerical"
+indexinfo[0].command[6].indexname "pto"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "pto"
+indexinfo[0].command[7].command "attribute"
+indexinfo[0].command[8].indexname "pto"
+indexinfo[0].command[8].command "numerical"
+indexinfo[0].command[9].indexname "keys"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[10].indexname "default"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "keys"
+indexinfo[0].command[11].command "lowercase"
+indexinfo[0].command[12].indexname "default"
+indexinfo[0].command[12].command "lowercase"
+indexinfo[0].command[13].indexname "keys"
+indexinfo[0].command[13].command "stem:SHORTEST"
+indexinfo[0].command[14].indexname "default"
+indexinfo[0].command[14].command "stem:SHORTEST"
+indexinfo[0].command[15].indexname "keys"
+indexinfo[0].command[15].command "normalize"
+indexinfo[0].command[16].indexname "default"
+indexinfo[0].command[16].command "normalize"
+indexinfo[0].command[17].indexname "mid"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "mid"
+indexinfo[0].command[18].command "attribute"
+indexinfo[0].command[19].indexname "mid"
+indexinfo[0].command[19].command "numerical"
+indexinfo[0].command[20].indexname "surl"
+indexinfo[0].command[20].command "index"
+indexinfo[0].command[21].indexname "userrate"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "userrate"
+indexinfo[0].command[22].command "numerical"
+indexinfo[0].command[23].indexname "pid"
+indexinfo[0].command[23].command "index"
+indexinfo[0].command[24].indexname "weight"
+indexinfo[0].command[24].command "index"
+indexinfo[0].command[25].indexname "weight"
+indexinfo[0].command[25].command "attribute"
+indexinfo[0].command[26].indexname "weight"
+indexinfo[0].command[26].command "numerical"
+indexinfo[0].command[27].indexname "url"
+indexinfo[0].command[27].command "index"
+indexinfo[0].command[28].indexname "isbn"
+indexinfo[0].command[28].command "index"
+indexinfo[0].command[29].indexname "fmt"
+indexinfo[0].command[29].command "index"
+indexinfo[0].command[30].indexname "fmt"
+indexinfo[0].command[30].command "lowercase"
+indexinfo[0].command[31].indexname "fmt"
+indexinfo[0].command[31].command "stem:SHORTEST"
+indexinfo[0].command[32].indexname "fmt"
+indexinfo[0].command[32].command "normalize"
+indexinfo[0].command[33].indexname "albumid"
+indexinfo[0].command[33].command "index"
+indexinfo[0].command[34].indexname "disp_song"
+indexinfo[0].command[34].command "index"
+indexinfo[0].command[35].indexname "song"
+indexinfo[0].command[35].command "index"
+indexinfo[0].command[36].indexname "song"
+indexinfo[0].command[36].command "lowercase"
+indexinfo[0].command[37].indexname "song"
+indexinfo[0].command[37].command "stem:SHORTEST"
+indexinfo[0].command[38].indexname "song"
+indexinfo[0].command[38].command "normalize"
+indexinfo[0].command[39].indexname "pfrom"
+indexinfo[0].command[39].command "index"
+indexinfo[0].command[40].indexname "pfrom"
+indexinfo[0].command[40].command "numerical"
+indexinfo[0].command[41].indexname "bgnpfrom"
+indexinfo[0].command[41].command "index"
+indexinfo[0].command[42].indexname "bgnpfrom"
+indexinfo[0].command[42].command "attribute"
+indexinfo[0].command[43].indexname "bgnpfrom"
+indexinfo[0].command[43].command "numerical"
+indexinfo[0].command[44].indexname "categories"
+indexinfo[0].command[44].command "index"
+indexinfo[0].command[45].indexname "categories"
+indexinfo[0].command[45].command "lowercase"
+indexinfo[0].command[46].indexname "categories"
+indexinfo[0].command[46].command "stem:SHORTEST"
+indexinfo[0].command[47].indexname "categories"
+indexinfo[0].command[47].command "normalize"
+indexinfo[0].command[48].indexname "data"
+indexinfo[0].command[48].command "index"
+indexinfo[0].command[49].indexname "numreview"
+indexinfo[0].command[49].command "index"
+indexinfo[0].command[50].indexname "numreview"
+indexinfo[0].command[50].command "numerical"
+indexinfo[0].command[51].indexname "bgnsellers"
+indexinfo[0].command[51].command "index"
+indexinfo[0].command[52].indexname "bgnsellers"
+indexinfo[0].command[52].command "numerical"
+indexinfo[0].command[53].indexname "image"
+indexinfo[0].command[53].command "index"
+indexinfo[0].command[54].indexname "artist"
+indexinfo[0].command[54].command "index"
+indexinfo[0].command[55].indexname "artist"
+indexinfo[0].command[55].command "lowercase"
+indexinfo[0].command[56].indexname "artist"
+indexinfo[0].command[56].command "stem:SHORTEST"
+indexinfo[0].command[57].indexname "artist"
+indexinfo[0].command[57].command "normalize"
+indexinfo[0].command[58].indexname "artistspid"
+indexinfo[0].command[58].command "index"
+indexinfo[0].command[59].indexname "title"
+indexinfo[0].command[59].command "index"
+indexinfo[0].command[60].indexname "title"
+indexinfo[0].command[60].command "lowercase"
+indexinfo[0].command[61].indexname "title"
+indexinfo[0].command[61].command "stem:SHORTEST"
+indexinfo[0].command[62].indexname "title"
+indexinfo[0].command[62].command "normalize"
+indexinfo[0].command[63].indexname "newestedition"
+indexinfo[0].command[63].command "index"
+indexinfo[0].command[64].indexname "newestedition"
+indexinfo[0].command[64].command "attribute"
+indexinfo[0].command[65].indexname "newestedition"
+indexinfo[0].command[65].command "numerical"
+indexinfo[0].command[66].indexname "bgnpto"
+indexinfo[0].command[66].command "index"
+indexinfo[0].command[67].indexname "year"
+indexinfo[0].command[67].command "index"
+indexinfo[0].command[68].indexname "year"
+indexinfo[0].command[68].command "attribute"
+indexinfo[0].command[69].indexname "year"
+indexinfo[0].command[69].command "numerical"
+indexinfo[0].command[70].indexname "did"
+indexinfo[0].command[70].command "index"
+indexinfo[0].command[71].indexname "did"
+indexinfo[0].command[71].command "attribute"
+indexinfo[0].command[72].indexname "did"
+indexinfo[0].command[72].command "numerical"
+indexinfo[0].command[73].indexname "scorekey"
+indexinfo[0].command[73].command "index"
+indexinfo[0].command[74].indexname "scorekey"
+indexinfo[0].command[74].command "attribute"
+indexinfo[0].command[75].indexname "scorekey"
+indexinfo[0].command[75].command "numerical"
+indexinfo[0].command[76].indexname "cbid"
+indexinfo[0].command[76].command "index"
+indexinfo[0].command[77].indexname "cbid"
+indexinfo[0].command[77].command "attribute"
+indexinfo[0].command[78].indexname "cbid"
+indexinfo[0].command[78].command "numerical"
+indexinfo[0].command[79].indexname "rankfeatures"
+indexinfo[0].command[79].command "index"
+indexinfo[0].command[80].indexname "summaryfeatures"
+indexinfo[0].command[80].command "index"
+indexinfo[0].command[81].indexname "bgndata"
+indexinfo[0].command[81].command "dynteaser"
+indexinfo[0].command[82].indexname "ew"
+indexinfo[0].command[82].command "highlight"
+indexinfo[0].command[83].indexname "song"
+indexinfo[0].command[83].command "dynteaser"
+indexinfo[0].command[84].indexname "bgnpto"
+indexinfo[0].command[84].command "dynteaser"
+indexinfo[0].command[85].indexname "bgnpto"
+indexinfo[0].command[85].command "highlight"
+indexinfo[0].command[9].indexname "ew"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[11].indexname "ew"
+indexinfo[0].command[11].command "lowercase"
+indexinfo[0].command[13].indexname "ew"
+indexinfo[0].command[13].command "stem:SHORTEST"
+indexinfo[0].command[15].indexname "ew"
+indexinfo[0].command[15].command "normalize"
diff --git a/config-model/src/test/derived/newrank/juniperrc.cfg b/config-model/src/test/derived/newrank/juniperrc.cfg
new file mode 100755
index 00000000000..b5d7438e081
--- /dev/null
+++ b/config-model/src/test/derived/newrank/juniperrc.cfg
@@ -0,0 +1,21 @@
+length 256
+max_matches 3
+min_length 128
+prefix true
+surround_max 128
+winsize 200
+winsize_fallback_multiplier 10.0
+max_match_candidates 1000
+stem_min_length 5
+stem_max_extend 3
+override[0].fieldname "ew"
+override[0].length 65536
+override[0].max_matches 1
+override[0].min_length 8192
+override[0].prefix true
+override[0].surround_max 65536
+override[0].winsize 200
+override[0].winsize_fallback_multiplier 10.0
+override[0].max_match_candidates 1000
+override[0].stem_min_length 5
+override[0].stem_max_extend 3 \ No newline at end of file
diff --git a/config-model/src/test/derived/newrank/newrank.sd b/config-model/src/test/derived/newrank/newrank.sd
new file mode 100644
index 00000000000..2a64c7cb07b
--- /dev/null
+++ b/config-model/src/test/derived/newrank/newrank.sd
@@ -0,0 +1,146 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search newrank{
+ document newrank{
+
+ field bgndata type string {
+ indexing: summary
+ summary: dynamic
+ }
+
+ field sales type int {
+ indexing: summary | attribute | index
+ }
+
+ field pto type int {
+ indexing: summary | attribute | index
+ }
+
+ field keys type string {
+ indexing: index
+ }
+
+ field mid type int {
+ indexing: summary | attribute | index
+ }
+
+ field ew type string {
+ indexing: summary | index
+ bolding: on
+ }
+
+ field surl type string {
+ indexing: summary
+ }
+
+ field userrate type int {
+ indexing: summary
+ }
+
+ field pid type string {
+ indexing: summary
+ }
+
+ field weight type float {
+ indexing: summary | attribute
+ }
+
+ field url type string {
+ indexing: summary
+ }
+
+ field isbn type string {
+ indexing: summary
+ }
+
+ field fmt type string {
+ indexing: summary | index
+ }
+
+ field albumid type string {
+ indexing: summary
+ }
+
+ field disp_song type string {
+ indexing: summary
+ }
+
+ field song type string {
+ indexing: summary | index
+ summary: dynamic
+ }
+
+ field pfrom type int {
+ indexing: summary
+ }
+
+ field bgnpfrom type float {
+ indexing: summary | attribute
+ }
+
+ field categories type string {
+ indexing: summary | index
+ }
+
+ field data type string {
+ indexing: summary
+ }
+
+ field numreview type int {
+ indexing: summary
+ }
+
+ field bgnsellers type int {
+ indexing: summary
+ }
+
+ field image type string {
+ indexing: summary
+ }
+
+ field artist type string {
+ indexing: summary | index
+ # index-to: artist, default
+ }
+
+ field artistspid type string {
+ indexing: summary
+ }
+
+ field title type string {
+ indexing: summary | index
+ }
+
+ field newestedition type int {
+ indexing: summary | attribute | index
+ }
+
+ field bgnpto type string {
+ indexing: summary
+ summary: dynamic
+ bolding: on
+ }
+
+ field year type int {
+ indexing: summary | attribute | index
+ }
+
+ field did type int {
+ indexing: summary | attribute | index
+ }
+
+ field scorekey type int {
+ indexing: summary | index
+ }
+
+ field cbid type int {
+ indexing: summary | attribute | index
+ }
+
+ }
+
+ fieldset default {
+ fields: keys, ew, song, title
+ }
+
+}
+
diff --git a/config-model/src/test/derived/newrank/rank-profiles.cfg b/config-model/src/test/derived/newrank/rank-profiles.cfg
new file mode 100644
index 00000000000..caca83a9a91
--- /dev/null
+++ b/config-model/src/test/derived/newrank/rank-profiles.cfg
@@ -0,0 +1,10 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/newrank/summary.cfg b/config-model/src/test/derived/newrank/summary.cfg
new file mode 100644
index 00000000000..7cb5a695a5d
--- /dev/null
+++ b/config-model/src/test/derived/newrank/summary.cfg
@@ -0,0 +1,97 @@
+defaultsummaryid 912980235
+classes[0].id 912980235
+classes[0].name "default"
+classes[0].fields[0].name "bgndata"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "sales"
+classes[0].fields[1].type "integer"
+classes[0].fields[2].name "pto"
+classes[0].fields[2].type "integer"
+classes[0].fields[3].name "mid"
+classes[0].fields[3].type "integer"
+classes[0].fields[4].name "ew"
+classes[0].fields[4].type "longstring"
+classes[0].fields[5].name "surl"
+classes[0].fields[5].type "longstring"
+classes[0].fields[6].name "userrate"
+classes[0].fields[6].type "integer"
+classes[0].fields[7].name "pid"
+classes[0].fields[7].type "longstring"
+classes[0].fields[8].name "weight"
+classes[0].fields[8].type "float"
+classes[0].fields[9].name "url"
+classes[0].fields[9].type "longstring"
+classes[0].fields[10].name "isbn"
+classes[0].fields[10].type "longstring"
+classes[0].fields[11].name "fmt"
+classes[0].fields[11].type "longstring"
+classes[0].fields[12].name "albumid"
+classes[0].fields[12].type "longstring"
+classes[0].fields[13].name "disp_song"
+classes[0].fields[13].type "longstring"
+classes[0].fields[14].name "song"
+classes[0].fields[14].type "longstring"
+classes[0].fields[15].name "pfrom"
+classes[0].fields[15].type "integer"
+classes[0].fields[16].name "bgnpfrom"
+classes[0].fields[16].type "float"
+classes[0].fields[17].name "categories"
+classes[0].fields[17].type "longstring"
+classes[0].fields[18].name "data"
+classes[0].fields[18].type "longstring"
+classes[0].fields[19].name "numreview"
+classes[0].fields[19].type "integer"
+classes[0].fields[20].name "bgnsellers"
+classes[0].fields[20].type "integer"
+classes[0].fields[21].name "image"
+classes[0].fields[21].type "longstring"
+classes[0].fields[22].name "artist"
+classes[0].fields[22].type "longstring"
+classes[0].fields[23].name "artistspid"
+classes[0].fields[23].type "longstring"
+classes[0].fields[24].name "title"
+classes[0].fields[24].type "longstring"
+classes[0].fields[25].name "newestedition"
+classes[0].fields[25].type "integer"
+classes[0].fields[26].name "bgnpto"
+classes[0].fields[26].type "longstring"
+classes[0].fields[27].name "year"
+classes[0].fields[27].type "integer"
+classes[0].fields[28].name "did"
+classes[0].fields[28].type "integer"
+classes[0].fields[29].name "scorekey"
+classes[0].fields[29].type "integer"
+classes[0].fields[30].name "cbid"
+classes[0].fields[30].type "integer"
+classes[0].fields[31].name "rankfeatures"
+classes[0].fields[31].type "featuredata"
+classes[0].fields[32].name "summaryfeatures"
+classes[0].fields[32].type "featuredata"
+classes[0].fields[33].name "documentid"
+classes[0].fields[33].type "longstring"
+classes[1].id 1606815285
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "sales"
+classes[1].fields[0].type "integer"
+classes[1].fields[1].name "pto"
+classes[1].fields[1].type "integer"
+classes[1].fields[2].name "mid"
+classes[1].fields[2].type "integer"
+classes[1].fields[3].name "weight"
+classes[1].fields[3].type "float"
+classes[1].fields[4].name "bgnpfrom"
+classes[1].fields[4].type "float"
+classes[1].fields[5].name "newestedition"
+classes[1].fields[5].type "integer"
+classes[1].fields[6].name "year"
+classes[1].fields[6].type "integer"
+classes[1].fields[7].name "did"
+classes[1].fields[7].type "integer"
+classes[1].fields[8].name "scorekey"
+classes[1].fields[8].type "integer"
+classes[1].fields[9].name "cbid"
+classes[1].fields[9].type "integer"
+classes[1].fields[10].name "rankfeatures"
+classes[1].fields[10].type "featuredata"
+classes[1].fields[11].name "summaryfeatures"
+classes[1].fields[11].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/newrank/summarymap.cfg b/config-model/src/test/derived/newrank/summarymap.cfg
new file mode 100644
index 00000000000..1ba02ac9060
--- /dev/null
+++ b/config-model/src/test/derived/newrank/summarymap.cfg
@@ -0,0 +1,49 @@
+defaultoutputclass -1
+override[0].field "bgndata"
+override[0].command "dynamicteaser"
+override[0].arguments "bgndata"
+override[1].field "sales"
+override[1].command "attribute"
+override[1].arguments "sales"
+override[2].field "pto"
+override[2].command "attribute"
+override[2].arguments "pto"
+override[3].field "mid"
+override[3].command "attribute"
+override[3].arguments "mid"
+override[4].field "ew"
+override[4].command "dynamicteaser"
+override[4].arguments "ew"
+override[5].field "weight"
+override[5].command "attribute"
+override[5].arguments "weight"
+override[6].field "song"
+override[6].command "dynamicteaser"
+override[6].arguments "song"
+override[7].field "bgnpfrom"
+override[7].command "attribute"
+override[7].arguments "bgnpfrom"
+override[8].field "newestedition"
+override[8].command "attribute"
+override[8].arguments "newestedition"
+override[9].field "bgnpto"
+override[9].command "dynamicteaser"
+override[9].arguments "bgnpto"
+override[10].field "year"
+override[10].command "attribute"
+override[10].arguments "year"
+override[11].field "did"
+override[11].command "attribute"
+override[11].arguments "did"
+override[12].field "scorekey"
+override[12].command "attribute"
+override[12].arguments "scorekey"
+override[13].field "cbid"
+override[13].command "attribute"
+override[13].arguments "cbid"
+override[14].field "rankfeatures"
+override[14].command "rankfeatures"
+override[14].arguments ""
+override[15].field "summaryfeatures"
+override[15].command "summaryfeatures"
+override[15].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/orderilscripts/ilscripts.cfg b/config-model/src/test/derived/orderilscripts/ilscripts.cfg
new file mode 100755
index 00000000000..656381d68a8
--- /dev/null
+++ b/config-model/src/test/derived/orderilscripts/ilscripts.cfg
@@ -0,0 +1,5 @@
+maxtermoccurrences 100
+ilscript[0].doctype "orderilscripts"
+ilscript[0].docfield[0] "foo"
+ilscript[0].content[0] "clear_state | guard { input foo | summary bar; }"
+ilscript[0].content[1] "clear_state | guard { input foo | tokenize normalize stem:\"SHORTEST\" | summary foo | index foo; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/orderilscripts/orderilscripts.sd b/config-model/src/test/derived/orderilscripts/orderilscripts.sd
new file mode 100755
index 00000000000..d1de519d200
--- /dev/null
+++ b/config-model/src/test/derived/orderilscripts/orderilscripts.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search orderilscripts {
+
+ document orderilscripts {
+ field foo type string {
+ indexing: summary | index
+ }
+ }
+
+ field bar type string {
+ indexing: input foo | summary
+ }
+}
diff --git a/config-model/src/test/derived/position_array/ilscripts.cfg b/config-model/src/test/derived/position_array/ilscripts.cfg
new file mode 100644
index 00000000000..dfe3827a1d5
--- /dev/null
+++ b/config-model/src/test/derived/position_array/ilscripts.cfg
@@ -0,0 +1,4 @@
+maxtermoccurrences 100
+ilscript[0].doctype "position_array"
+ilscript[0].docfield[0] "pos"
+ilscript[0].content[0] "clear_state | guard { input pos | for_each { zcurve } | attribute pos_zcurve; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_array/index-info.cfg b/config-model/src/test/derived/position_array/index-info.cfg
new file mode 100644
index 00000000000..8ae074d337f
--- /dev/null
+++ b/config-model/src/test/derived/position_array/index-info.cfg
@@ -0,0 +1,39 @@
+indexinfo[0].name "position_array"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "pos.x"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "pos.x"
+indexinfo[0].command[3].command "numerical"
+indexinfo[0].command[4].indexname "pos.y"
+indexinfo[0].command[4].command "index"
+indexinfo[0].command[5].indexname "pos.y"
+indexinfo[0].command[5].command "numerical"
+indexinfo[0].command[6].indexname "pos"
+indexinfo[0].command[6].command "default-position"
+indexinfo[0].command[7].indexname "pos"
+indexinfo[0].command[7].command "index"
+indexinfo[0].command[8].indexname "pos"
+indexinfo[0].command[8].command "multivalue"
+indexinfo[0].command[9].indexname "pos.distance"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[10].indexname "pos.distance"
+indexinfo[0].command[10].command "numerical"
+indexinfo[0].command[11].indexname "pos.position"
+indexinfo[0].command[11].command "index"
+indexinfo[0].command[12].indexname "pos.position"
+indexinfo[0].command[12].command "multivalue"
+indexinfo[0].command[13].indexname "pos_zcurve"
+indexinfo[0].command[13].command "index"
+indexinfo[0].command[14].indexname "pos_zcurve"
+indexinfo[0].command[14].command "multivalue"
+indexinfo[0].command[15].indexname "pos_zcurve"
+indexinfo[0].command[15].command "attribute"
+indexinfo[0].command[16].indexname "pos_zcurve"
+indexinfo[0].command[16].command "fast-search"
+indexinfo[0].command[17].indexname "rankfeatures"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "summaryfeatures"
+indexinfo[0].command[18].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_array/position_array.sd b/config-model/src/test/derived/position_array/position_array.sd
new file mode 100644
index 00000000000..e5042354ade
--- /dev/null
+++ b/config-model/src/test/derived/position_array/position_array.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_array {
+ document position_array {
+ field pos type array<position> {
+ indexing: attribute
+ }
+ }
+}
diff --git a/config-model/src/test/derived/position_attribute/ilscripts.cfg b/config-model/src/test/derived/position_attribute/ilscripts.cfg
new file mode 100644
index 00000000000..ea6d6c4080b
--- /dev/null
+++ b/config-model/src/test/derived/position_attribute/ilscripts.cfg
@@ -0,0 +1,4 @@
+maxtermoccurrences 100
+ilscript[0].doctype "position_attribute"
+ilscript[0].docfield[0] "pos"
+ilscript[0].content[0] "clear_state | guard { input pos | zcurve | attribute pos_zcurve; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_attribute/index-info.cfg b/config-model/src/test/derived/position_attribute/index-info.cfg
new file mode 100644
index 00000000000..99d2c621722
--- /dev/null
+++ b/config-model/src/test/derived/position_attribute/index-info.cfg
@@ -0,0 +1,37 @@
+indexinfo[0].name "position_attribute"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "pos.x"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "pos.x"
+indexinfo[0].command[3].command "numerical"
+indexinfo[0].command[4].indexname "pos.y"
+indexinfo[0].command[4].command "index"
+indexinfo[0].command[5].indexname "pos.y"
+indexinfo[0].command[5].command "numerical"
+indexinfo[0].command[6].indexname "pos"
+indexinfo[0].command[6].command "default-position"
+indexinfo[0].command[7].indexname "pos"
+indexinfo[0].command[7].command "index"
+indexinfo[0].command[8].indexname "pos.distance"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "pos.distance"
+indexinfo[0].command[9].command "numerical"
+indexinfo[0].command[10].indexname "pos.position"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "pos.position"
+indexinfo[0].command[11].command "multivalue"
+indexinfo[0].command[12].indexname "pos_zcurve"
+indexinfo[0].command[12].command "index"
+indexinfo[0].command[13].indexname "pos_zcurve"
+indexinfo[0].command[13].command "attribute"
+indexinfo[0].command[14].indexname "pos_zcurve"
+indexinfo[0].command[14].command "fast-search"
+indexinfo[0].command[15].indexname "pos_zcurve"
+indexinfo[0].command[15].command "numerical"
+indexinfo[0].command[16].indexname "rankfeatures"
+indexinfo[0].command[16].command "index"
+indexinfo[0].command[17].indexname "summaryfeatures"
+indexinfo[0].command[17].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_attribute/position_attribute.sd b/config-model/src/test/derived/position_attribute/position_attribute.sd
new file mode 100644
index 00000000000..6841c7847e0
--- /dev/null
+++ b/config-model/src/test/derived/position_attribute/position_attribute.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_attribute {
+ document position_attribute {
+ field pos type position {
+ indexing: attribute
+ }
+ }
+}
diff --git a/config-model/src/test/derived/position_extra/ilscripts.cfg b/config-model/src/test/derived/position_extra/ilscripts.cfg
new file mode 100644
index 00000000000..33d61e55465
--- /dev/null
+++ b/config-model/src/test/derived/position_extra/ilscripts.cfg
@@ -0,0 +1,4 @@
+maxtermoccurrences 100
+ilscript[0].doctype "position_extra"
+ilscript[0].docfield[0] "pos_str"
+ilscript[0].content[0] "clear_state | guard { input pos_str | to_pos | zcurve | attribute pos_ext_zcurve; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_extra/index-info.cfg b/config-model/src/test/derived/position_extra/index-info.cfg
new file mode 100644
index 00000000000..649dacc24e0
--- /dev/null
+++ b/config-model/src/test/derived/position_extra/index-info.cfg
@@ -0,0 +1,31 @@
+indexinfo[0].name "position_extra"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "pos_str"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "pos_ext"
+indexinfo[0].command[3].command "default-position"
+indexinfo[0].command[4].indexname "pos_ext"
+indexinfo[0].command[4].command "index"
+indexinfo[0].command[5].indexname "pos_ext.distance"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "pos_ext.distance"
+indexinfo[0].command[6].command "numerical"
+indexinfo[0].command[7].indexname "pos_ext.position"
+indexinfo[0].command[7].command "index"
+indexinfo[0].command[8].indexname "pos_ext.position"
+indexinfo[0].command[8].command "multivalue"
+indexinfo[0].command[9].indexname "pos_ext_zcurve"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[10].indexname "pos_ext_zcurve"
+indexinfo[0].command[10].command "attribute"
+indexinfo[0].command[11].indexname "pos_ext_zcurve"
+indexinfo[0].command[11].command "fast-search"
+indexinfo[0].command[12].indexname "pos_ext_zcurve"
+indexinfo[0].command[12].command "numerical"
+indexinfo[0].command[13].indexname "rankfeatures"
+indexinfo[0].command[13].command "index"
+indexinfo[0].command[14].indexname "summaryfeatures"
+indexinfo[0].command[14].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_extra/position_extra.sd b/config-model/src/test/derived/position_extra/position_extra.sd
new file mode 100644
index 00000000000..0254b4ed31d
--- /dev/null
+++ b/config-model/src/test/derived/position_extra/position_extra.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_extra {
+ document position_extra {
+ field pos_str type string {
+
+ }
+ }
+ field pos_ext type position {
+ indexing: input pos_str | to_pos | attribute
+ }
+}
diff --git a/config-model/src/test/derived/position_nosummary/position_nosummary.sd b/config-model/src/test/derived/position_nosummary/position_nosummary.sd
new file mode 100644
index 00000000000..9b902b4d608
--- /dev/null
+++ b/config-model/src/test/derived/position_nosummary/position_nosummary.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_nosummary {
+ document position_nosummary {
+ field pos type position {
+ indexing: attribute
+ }
+ }
+}
diff --git a/config-model/src/test/derived/position_nosummary/summary.cfg b/config-model/src/test/derived/position_nosummary/summary.cfg
new file mode 100644
index 00000000000..fbf091209b9
--- /dev/null
+++ b/config-model/src/test/derived/position_nosummary/summary.cfg
@@ -0,0 +1,21 @@
+defaultsummaryid 1727020212
+classes[0].id 1727020212
+classes[0].name "default"
+classes[0].fields[0].name "pos.position"
+classes[0].fields[0].type "xmlstring"
+classes[0].fields[1].name "pos.distance"
+classes[0].fields[1].type "integer"
+classes[0].fields[2].name "rankfeatures"
+classes[0].fields[2].type "featuredata"
+classes[0].fields[3].name "summaryfeatures"
+classes[0].fields[3].type "featuredata"
+classes[0].fields[4].name "documentid"
+classes[0].fields[4].type "longstring"
+classes[1].id 1530141163
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "pos_zcurve"
+classes[1].fields[0].type "int64"
+classes[1].fields[1].name "rankfeatures"
+classes[1].fields[1].type "featuredata"
+classes[1].fields[2].name "summaryfeatures"
+classes[1].fields[2].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_nosummary/summarymap.cfg b/config-model/src/test/derived/position_nosummary/summarymap.cfg
new file mode 100644
index 00000000000..2e8ada7daa0
--- /dev/null
+++ b/config-model/src/test/derived/position_nosummary/summarymap.cfg
@@ -0,0 +1,16 @@
+defaultoutputclass -1
+override[0].field "pos.position"
+override[0].command "positions"
+override[0].arguments "pos_zcurve"
+override[1].field "pos.distance"
+override[1].command "absdist"
+override[1].arguments "pos_zcurve"
+override[2].field "rankfeatures"
+override[2].command "rankfeatures"
+override[2].arguments ""
+override[3].field "summaryfeatures"
+override[3].command "summaryfeatures"
+override[3].arguments ""
+override[4].field "pos_zcurve"
+override[4].command "attribute"
+override[4].arguments "pos_zcurve" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_summary/position_summary.sd b/config-model/src/test/derived/position_summary/position_summary.sd
new file mode 100644
index 00000000000..ccc136f635a
--- /dev/null
+++ b/config-model/src/test/derived/position_summary/position_summary.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_summary {
+ document position_summary {
+ field pos type position {
+ indexing: attribute | summary
+ }
+ }
+}
diff --git a/config-model/src/test/derived/position_summary/summary.cfg b/config-model/src/test/derived/position_summary/summary.cfg
new file mode 100644
index 00000000000..708fbe9ca18
--- /dev/null
+++ b/config-model/src/test/derived/position_summary/summary.cfg
@@ -0,0 +1,23 @@
+defaultsummaryid 230670304
+classes[0].id 230670304
+classes[0].name "default"
+classes[0].fields[0].name "pos"
+classes[0].fields[0].type "jsonstring"
+classes[0].fields[1].name "pos.position"
+classes[0].fields[1].type "xmlstring"
+classes[0].fields[2].name "pos.distance"
+classes[0].fields[2].type "integer"
+classes[0].fields[3].name "rankfeatures"
+classes[0].fields[3].type "featuredata"
+classes[0].fields[4].name "summaryfeatures"
+classes[0].fields[4].type "featuredata"
+classes[0].fields[5].name "documentid"
+classes[0].fields[5].type "longstring"
+classes[1].id 1530141163
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "pos_zcurve"
+classes[1].fields[0].type "int64"
+classes[1].fields[1].name "rankfeatures"
+classes[1].fields[1].type "featuredata"
+classes[1].fields[2].name "summaryfeatures"
+classes[1].fields[2].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_summary/summarymap.cfg b/config-model/src/test/derived/position_summary/summarymap.cfg
new file mode 100644
index 00000000000..caf88368044
--- /dev/null
+++ b/config-model/src/test/derived/position_summary/summarymap.cfg
@@ -0,0 +1,19 @@
+defaultoutputclass -1
+override[0].field "pos"
+override[0].command "geopos"
+override[0].arguments "pos_zcurve"
+override[1].field "pos.position"
+override[1].command "positions"
+override[1].arguments "pos_zcurve"
+override[2].field "pos.distance"
+override[2].command "absdist"
+override[2].arguments "pos_zcurve"
+override[3].field "rankfeatures"
+override[3].command "rankfeatures"
+override[3].arguments ""
+override[4].field "summaryfeatures"
+override[4].command "summaryfeatures"
+override[4].arguments ""
+override[5].field "pos_zcurve"
+override[5].command "attribute"
+override[5].arguments "pos_zcurve" \ No newline at end of file
diff --git a/config-model/src/test/derived/position_summary/vsmsummary.cfg b/config-model/src/test/derived/position_summary/vsmsummary.cfg
new file mode 100644
index 00000000000..770a228ed52
--- /dev/null
+++ b/config-model/src/test/derived/position_summary/vsmsummary.cfg
@@ -0,0 +1,14 @@
+outputclass ""
+fieldmap[0].summary "pos"
+fieldmap[0].document[0].field "pos"
+fieldmap[0].command NONE
+fieldmap[1].summary "pos.position"
+fieldmap[1].document[0].field "pos_zcurve"
+fieldmap[1].command NONE
+fieldmap[2].summary "pos.distance"
+fieldmap[2].document[0].field "pos_zcurve"
+fieldmap[2].command NONE
+fieldmap[3].summary "rankfeatures"
+fieldmap[3].command NONE
+fieldmap[4].summary "summaryfeatures"
+fieldmap[4].command NONE \ No newline at end of file
diff --git a/config-model/src/test/derived/predicate_attribute/attributes.cfg b/config-model/src/test/derived/predicate_attribute/attributes.cfg
new file mode 100644
index 00000000000..49d86516930
--- /dev/null
+++ b/config-model/src/test/derived/predicate_attribute/attributes.cfg
@@ -0,0 +1,19 @@
+attribute[0].name "some_predicate_field"
+attribute[0].datatype PREDICATE
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 5
+attribute[0].lowerbound 3
+attribute[0].upperbound 200
+attribute[0].densepostinglistthreshold 0.2
+attribute[0].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/predicate_attribute/index-info.cfg b/config-model/src/test/derived/predicate_attribute/index-info.cfg
new file mode 100644
index 00000000000..b2c5f9c4d30
--- /dev/null
+++ b/config-model/src/test/derived/predicate_attribute/index-info.cfg
@@ -0,0 +1,15 @@
+indexinfo[0].name "predicate_type"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "some_predicate_field"
+indexinfo[0].command[2].command "predicate-bounds [3..200]"
+indexinfo[0].command[3].indexname "some_predicate_field"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "some_predicate_field"
+indexinfo[0].command[4].command "attribute"
+indexinfo[0].command[5].indexname "rankfeatures"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "summaryfeatures"
+indexinfo[0].command[6].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/predicate_attribute/predicate_attribute.sd b/config-model/src/test/derived/predicate_attribute/predicate_attribute.sd
new file mode 100644
index 00000000000..5b509fc1210
--- /dev/null
+++ b/config-model/src/test/derived/predicate_attribute/predicate_attribute.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search predicate_type {
+ document predicate_type {
+ field some_predicate_field type predicate {
+ indexing: attribute | summary
+ index {
+ arity: 5
+ lower-bound: 3
+ upper-bound: 200
+ dense-posting-list-threshold: 0.2
+ }
+ }
+ }
+}
diff --git a/config-model/src/test/derived/predicate_attribute/summary.cfg b/config-model/src/test/derived/predicate_attribute/summary.cfg
new file mode 100644
index 00000000000..e10af36caf5
--- /dev/null
+++ b/config-model/src/test/derived/predicate_attribute/summary.cfg
@@ -0,0 +1,17 @@
+defaultsummaryid 1391971216
+classes[0].id 1391971216
+classes[0].name "default"
+classes[0].fields[0].name "some_predicate_field"
+classes[0].fields[0].type "string"
+classes[0].fields[1].name "rankfeatures"
+classes[0].fields[1].type "featuredata"
+classes[0].fields[2].name "summaryfeatures"
+classes[0].fields[2].type "featuredata"
+classes[0].fields[3].name "documentid"
+classes[0].fields[3].type "longstring"
+classes[1].id 1274088866
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "rankfeatures"
+classes[1].fields[0].type "featuredata"
+classes[1].fields[1].name "summaryfeatures"
+classes[1].fields[1].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/predicate_attribute/summarymap.cfg b/config-model/src/test/derived/predicate_attribute/summarymap.cfg
new file mode 100644
index 00000000000..42b6e811ee6
--- /dev/null
+++ b/config-model/src/test/derived/predicate_attribute/summarymap.cfg
@@ -0,0 +1,7 @@
+defaultoutputclass -1
+override[0].field "rankfeatures"
+override[0].command "rankfeatures"
+override[0].arguments ""
+override[1].field "summaryfeatures"
+override[1].command "summaryfeatures"
+override[1].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/prefixexactattribute/attributes.cfg b/config-model/src/test/derived/prefixexactattribute/attributes.cfg
new file mode 100644
index 00000000000..257ebcdebab
--- /dev/null
+++ b/config-model/src/test/derived/prefixexactattribute/attributes.cfg
@@ -0,0 +1,38 @@
+attribute[0].name "attributefield1"
+attribute[0].datatype STRING
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "attributefield2"
+attribute[1].datatype STRING
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/prefixexactattribute/documentmanager.cfg b/config-model/src/test/derived/prefixexactattribute/documentmanager.cfg
new file mode 100644
index 00000000000..1b51178e6d8
--- /dev/null
+++ b/config-model/src/test/derived/prefixexactattribute/documentmanager.cfg
@@ -0,0 +1,52 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -739138930
+datatype[1].structtype[0].name "prefixexactattribute.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "indexfield0"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[1].structtype[0].field[1].name "attributefield1"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "attributefield2"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[1].structtype[0].field[3].name "indexfield1"
+datatype[1].structtype[0].field[3].datatype 2
+datatype[1].structtype[0].field[4].name "indexfield2"
+datatype[1].structtype[0].field[4].datatype 2
+datatype[1].structtype[0].field[5].name "rankfeatures"
+datatype[1].structtype[0].field[5].datatype 2
+datatype[1].structtype[0].field[6].name "summaryfeatures"
+datatype[1].structtype[0].field[6].datatype 2
+datatype[2].id -480519133
+datatype[2].structtype[0].name "prefixexactattribute.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[3].id -1812793455
+datatype[3].documenttype[0].name "prefixexactattribute"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct -739138930
+datatype[3].documenttype[0].bodystruct -480519133
+datatype[3].documenttype[0].fieldsets{[document]}.fields[0] "attributefield1"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[1] "attributefield2"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[2] "indexfield0"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[3] "indexfield1"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[4] "indexfield2"
diff --git a/config-model/src/test/derived/prefixexactattribute/ilscripts.cfg b/config-model/src/test/derived/prefixexactattribute/ilscripts.cfg
new file mode 100644
index 00000000000..76c9bf9abf5
--- /dev/null
+++ b/config-model/src/test/derived/prefixexactattribute/ilscripts.cfg
@@ -0,0 +1,12 @@
+maxtermoccurrences 100
+ilscript[0].doctype "prefixexactattribute"
+ilscript[0].docfield[0] "indexfield0"
+ilscript[0].docfield[1] "attributefield1"
+ilscript[0].docfield[2] "attributefield2"
+ilscript[0].docfield[3] "indexfield1"
+ilscript[0].docfield[4] "indexfield2"
+ilscript[0].content[0] "clear_state | guard { input indexfield0 | tokenize normalize stem:\"SHORTEST\" | index indexfield0; }"
+ilscript[0].content[1] "clear_state | guard { input attributefield1 | attribute attributefield1; }"
+ilscript[0].content[2] "clear_state | guard { input attributefield2 | attribute attributefield2; }"
+ilscript[0].content[3] "clear_state | guard { input indexfield1 | exact | index indexfield1; }"
+ilscript[0].content[4] "clear_state | guard { input indexfield2 | exact | index indexfield2; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/prefixexactattribute/index-info.cfg b/config-model/src/test/derived/prefixexactattribute/index-info.cfg
new file mode 100644
index 00000000000..74bf641990c
--- /dev/null
+++ b/config-model/src/test/derived/prefixexactattribute/index-info.cfg
@@ -0,0 +1,41 @@
+indexinfo[0].name "prefixexactattribute"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "indexfield0"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "indexfield0"
+indexinfo[0].command[3].command "lowercase"
+indexinfo[0].command[4].indexname "indexfield0"
+indexinfo[0].command[4].command "stem:SHORTEST"
+indexinfo[0].command[5].indexname "indexfield0"
+indexinfo[0].command[5].command "normalize"
+indexinfo[0].command[6].indexname "attributefield1"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "attributefield1"
+indexinfo[0].command[7].command "attribute"
+indexinfo[0].command[8].indexname "attributefield1"
+indexinfo[0].command[8].command "exact @"
+indexinfo[0].command[9].indexname "attributefield2"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[10].indexname "attributefield2"
+indexinfo[0].command[10].command "attribute"
+indexinfo[0].command[11].indexname "attributefield2"
+indexinfo[0].command[11].command "exact @"
+indexinfo[0].command[12].indexname "indexfield1"
+indexinfo[0].command[12].command "index"
+indexinfo[0].command[13].indexname "indexfield1"
+indexinfo[0].command[13].command "lowercase"
+indexinfo[0].command[14].indexname "indexfield1"
+indexinfo[0].command[14].command "exact @"
+indexinfo[0].command[15].indexname "indexfield2"
+indexinfo[0].command[15].command "index"
+indexinfo[0].command[16].indexname "indexfield2"
+indexinfo[0].command[16].command "lowercase"
+indexinfo[0].command[17].indexname "indexfield2"
+indexinfo[0].command[17].command "exact @"
+indexinfo[0].command[18].indexname "rankfeatures"
+indexinfo[0].command[18].command "index"
+indexinfo[0].command[19].indexname "summaryfeatures"
+indexinfo[0].command[19].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/prefixexactattribute/prefixexactattribute.sd b/config-model/src/test/derived/prefixexactattribute/prefixexactattribute.sd
new file mode 100644
index 00000000000..d2835a0e0f2
--- /dev/null
+++ b/config-model/src/test/derived/prefixexactattribute/prefixexactattribute.sd
@@ -0,0 +1,52 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search prefixexactattribute {
+
+ document prefixexactattribute {
+
+ field indexfield0 type string {
+ indexing: index
+ match {
+ prefix
+ max-length: 79
+ }
+ }
+
+ field attributefield1 type string {
+ indexing: attribute
+ match {
+ prefix
+ exact
+ exact-terminator: "@"
+ }
+ }
+
+ field attributefield2 type string {
+ indexing: attribute
+ match {
+ exact
+ prefix
+ exact-terminator: "@"
+ }
+ }
+
+ field indexfield1 type string {
+ indexing: index
+ match {
+ exact
+ prefix
+ exact-terminator: "@"
+ }
+ }
+
+ # Old style - deprecated
+ field indexfield2 type string {
+ indexing: index
+ index: prefix
+ match {
+ exact
+ exact-terminator: "@"
+ }
+ }
+
+ }
+}
diff --git a/config-model/src/test/derived/prefixexactattribute/vsmfields.cfg b/config-model/src/test/derived/prefixexactattribute/vsmfields.cfg
new file mode 100644
index 00000000000..598dfc0b6b1
--- /dev/null
+++ b/config-model/src/test/derived/prefixexactattribute/vsmfields.cfg
@@ -0,0 +1,38 @@
+documentverificationlevel 0
+searchall 1
+fieldspec[0].name "indexfield0"
+fieldspec[0].searchmethod AUTOUTF8
+fieldspec[0].arg1 "prefix"
+fieldspec[0].maxlength 79
+fieldspec[0].fieldtype INDEX
+fieldspec[1].name "attributefield1"
+fieldspec[1].searchmethod AUTOUTF8
+fieldspec[1].arg1 "exact"
+fieldspec[1].maxlength 1048576
+fieldspec[1].fieldtype ATTRIBUTE
+fieldspec[2].name "attributefield2"
+fieldspec[2].searchmethod AUTOUTF8
+fieldspec[2].arg1 "exact"
+fieldspec[2].maxlength 1048576
+fieldspec[2].fieldtype ATTRIBUTE
+fieldspec[3].name "indexfield1"
+fieldspec[3].searchmethod AUTOUTF8
+fieldspec[3].arg1 "exact"
+fieldspec[3].maxlength 1048576
+fieldspec[3].fieldtype INDEX
+fieldspec[4].name "indexfield2"
+fieldspec[4].searchmethod AUTOUTF8
+fieldspec[4].arg1 "exact"
+fieldspec[4].maxlength 1048576
+fieldspec[4].fieldtype INDEX
+documenttype[0].name "prefixexactattribute"
+documenttype[0].index[0].name "indexfield0"
+documenttype[0].index[0].field[0].name "indexfield0"
+documenttype[0].index[1].name "attributefield1"
+documenttype[0].index[1].field[0].name "attributefield1"
+documenttype[0].index[2].name "attributefield2"
+documenttype[0].index[2].field[0].name "attributefield2"
+documenttype[0].index[3].name "indexfield1"
+documenttype[0].index[3].field[0].name "indexfield1"
+documenttype[0].index[4].name "indexfield2"
+documenttype[0].index[4].field[0].name "indexfield2" \ No newline at end of file
diff --git a/config-model/src/test/derived/rankexpression/macro.expression b/config-model/src/test/derived/rankexpression/macro.expression
new file mode 100644
index 00000000000..054b025b2e7
--- /dev/null
+++ b/config-model/src/test/derived/rankexpression/macro.expression
@@ -0,0 +1 @@
+703 * fieldMatch(fromfile).completeness \ No newline at end of file
diff --git a/config-model/src/test/derived/rankexpression/overflow.expression b/config-model/src/test/derived/rankexpression/overflow.expression
new file mode 100644
index 00000000000..4d3a6faeeb5
--- /dev/null
+++ b/config-model/src/test/derived/rankexpression/overflow.expression
@@ -0,0 +1,300 @@
+feature1(argument1, argument2, argument3, argument4).output +
+feature2(argument1, argument2, argument3, argument4).output +
+feature3(argument1, argument2, argument3, argument4).output +
+feature4(argument1, argument2, argument3, argument4).output +
+feature5(argument1, argument2, argument3, argument4).output +
+feature6(argument1, argument2, argument3, argument4).output +
+feature7(argument1, argument2, argument3, argument4).output +
+feature8(argument1, argument2, argument3, argument4).output +
+feature9(argument1, argument2, argument3, argument4).output +
+feature10(argument1, argument2, argument3, argument4).output +
+feature11(argument1, argument2, argument3, argument4).output +
+feature12(argument1, argument2, argument3, argument4).output +
+feature13(argument1, argument2, argument3, argument4).output +
+feature14(argument1, argument2, argument3, argument4).output +
+feature15(argument1, argument2, argument3, argument4).output +
+feature16(argument1, argument2, argument3, argument4).output +
+feature17(argument1, argument2, argument3, argument4).output +
+feature18(argument1, argument2, argument3, argument4).output +
+feature19(argument1, argument2, argument3, argument4).output +
+feature20(argument1, argument2, argument3, argument4).output +
+feature21(argument1, argument2, argument3, argument4).output +
+feature22(argument1, argument2, argument3, argument4).output +
+feature23(argument1, argument2, argument3, argument4).output +
+feature24(argument1, argument2, argument3, argument4).output +
+feature25(argument1, argument2, argument3, argument4).output +
+feature26(argument1, argument2, argument3, argument4).output +
+feature27(argument1, argument2, argument3, argument4).output +
+feature28(argument1, argument2, argument3, argument4).output +
+feature29(argument1, argument2, argument3, argument4).output +
+feature30(argument1, argument2, argument3, argument4).output +
+feature31(argument1, argument2, argument3, argument4).output +
+feature32(argument1, argument2, argument3, argument4).output +
+feature33(argument1, argument2, argument3, argument4).output +
+feature34(argument1, argument2, argument3, argument4).output +
+feature35(argument1, argument2, argument3, argument4).output +
+feature36(argument1, argument2, argument3, argument4).output +
+feature37(argument1, argument2, argument3, argument4).output +
+feature38(argument1, argument2, argument3, argument4).output +
+feature39(argument1, argument2, argument3, argument4).output +
+feature40(argument1, argument2, argument3, argument4).output +
+feature41(argument1, argument2, argument3, argument4).output +
+feature42(argument1, argument2, argument3, argument4).output +
+feature43(argument1, argument2, argument3, argument4).output +
+feature44(argument1, argument2, argument3, argument4).output +
+feature45(argument1, argument2, argument3, argument4).output +
+feature46(argument1, argument2, argument3, argument4).output +
+feature47(argument1, argument2, argument3, argument4).output +
+feature48(argument1, argument2, argument3, argument4).output +
+feature49(argument1, argument2, argument3, argument4).output +
+feature50(argument1, argument2, argument3, argument4).output +
+feature51(argument1, argument2, argument3, argument4).output +
+feature52(argument1, argument2, argument3, argument4).output +
+feature53(argument1, argument2, argument3, argument4).output +
+feature54(argument1, argument2, argument3, argument4).output +
+feature55(argument1, argument2, argument3, argument4).output +
+feature56(argument1, argument2, argument3, argument4).output +
+feature57(argument1, argument2, argument3, argument4).output +
+feature58(argument1, argument2, argument3, argument4).output +
+feature59(argument1, argument2, argument3, argument4).output +
+feature60(argument1, argument2, argument3, argument4).output +
+feature61(argument1, argument2, argument3, argument4).output +
+feature62(argument1, argument2, argument3, argument4).output +
+feature63(argument1, argument2, argument3, argument4).output +
+feature64(argument1, argument2, argument3, argument4).output +
+feature65(argument1, argument2, argument3, argument4).output +
+feature66(argument1, argument2, argument3, argument4).output +
+feature67(argument1, argument2, argument3, argument4).output +
+feature68(argument1, argument2, argument3, argument4).output +
+feature69(argument1, argument2, argument3, argument4).output +
+feature70(argument1, argument2, argument3, argument4).output +
+feature71(argument1, argument2, argument3, argument4).output +
+feature72(argument1, argument2, argument3, argument4).output +
+feature73(argument1, argument2, argument3, argument4).output +
+feature74(argument1, argument2, argument3, argument4).output +
+feature75(argument1, argument2, argument3, argument4).output +
+feature76(argument1, argument2, argument3, argument4).output +
+feature77(argument1, argument2, argument3, argument4).output +
+feature78(argument1, argument2, argument3, argument4).output +
+feature79(argument1, argument2, argument3, argument4).output +
+feature80(argument1, argument2, argument3, argument4).output +
+feature81(argument1, argument2, argument3, argument4).output +
+feature82(argument1, argument2, argument3, argument4).output +
+feature83(argument1, argument2, argument3, argument4).output +
+feature84(argument1, argument2, argument3, argument4).output +
+feature85(argument1, argument2, argument3, argument4).output +
+feature86(argument1, argument2, argument3, argument4).output +
+feature87(argument1, argument2, argument3, argument4).output +
+feature88(argument1, argument2, argument3, argument4).output +
+feature89(argument1, argument2, argument3, argument4).output +
+feature90(argument1, argument2, argument3, argument4).output +
+feature91(argument1, argument2, argument3, argument4).output +
+feature92(argument1, argument2, argument3, argument4).output +
+feature93(argument1, argument2, argument3, argument4).output +
+feature94(argument1, argument2, argument3, argument4).output +
+feature95(argument1, argument2, argument3, argument4).output +
+feature96(argument1, argument2, argument3, argument4).output +
+feature97(argument1, argument2, argument3, argument4).output +
+feature98(argument1, argument2, argument3, argument4).output +
+feature99(argument1, argument2, argument3, argument4).output +
+feature100(argument1, argument2, argument3, argument4).output +
+feature101(argument1, argument2, argument3, argument4).output +
+feature102(argument1, argument2, argument3, argument4).output +
+feature103(argument1, argument2, argument3, argument4).output +
+feature104(argument1, argument2, argument3, argument4).output +
+feature105(argument1, argument2, argument3, argument4).output +
+feature106(argument1, argument2, argument3, argument4).output +
+feature107(argument1, argument2, argument3, argument4).output +
+feature108(argument1, argument2, argument3, argument4).output +
+feature109(argument1, argument2, argument3, argument4).output +
+feature110(argument1, argument2, argument3, argument4).output +
+feature111(argument1, argument2, argument3, argument4).output +
+feature112(argument1, argument2, argument3, argument4).output +
+feature113(argument1, argument2, argument3, argument4).output +
+feature114(argument1, argument2, argument3, argument4).output +
+feature115(argument1, argument2, argument3, argument4).output +
+feature116(argument1, argument2, argument3, argument4).output +
+feature117(argument1, argument2, argument3, argument4).output +
+feature118(argument1, argument2, argument3, argument4).output +
+feature119(argument1, argument2, argument3, argument4).output +
+feature120(argument1, argument2, argument3, argument4).output +
+feature121(argument1, argument2, argument3, argument4).output +
+feature122(argument1, argument2, argument3, argument4).output +
+feature123(argument1, argument2, argument3, argument4).output +
+feature124(argument1, argument2, argument3, argument4).output +
+feature125(argument1, argument2, argument3, argument4).output +
+feature126(argument1, argument2, argument3, argument4).output +
+feature127(argument1, argument2, argument3, argument4).output +
+feature128(argument1, argument2, argument3, argument4).output +
+feature129(argument1, argument2, argument3, argument4).output +
+feature130(argument1, argument2, argument3, argument4).output +
+feature131(argument1, argument2, argument3, argument4).output +
+feature132(argument1, argument2, argument3, argument4).output +
+feature133(argument1, argument2, argument3, argument4).output +
+feature134(argument1, argument2, argument3, argument4).output +
+feature135(argument1, argument2, argument3, argument4).output +
+feature136(argument1, argument2, argument3, argument4).output +
+feature137(argument1, argument2, argument3, argument4).output +
+feature138(argument1, argument2, argument3, argument4).output +
+feature139(argument1, argument2, argument3, argument4).output +
+feature140(argument1, argument2, argument3, argument4).output +
+feature141(argument1, argument2, argument3, argument4).output +
+feature142(argument1, argument2, argument3, argument4).output +
+feature143(argument1, argument2, argument3, argument4).output +
+feature144(argument1, argument2, argument3, argument4).output +
+feature145(argument1, argument2, argument3, argument4).output +
+feature146(argument1, argument2, argument3, argument4).output +
+feature147(argument1, argument2, argument3, argument4).output +
+feature148(argument1, argument2, argument3, argument4).output +
+feature149(argument1, argument2, argument3, argument4).output +
+feature150(argument1, argument2, argument3, argument4).output +
+feature151(argument1, argument2, argument3, argument4).output +
+feature152(argument1, argument2, argument3, argument4).output +
+feature153(argument1, argument2, argument3, argument4).output +
+feature154(argument1, argument2, argument3, argument4).output +
+feature155(argument1, argument2, argument3, argument4).output +
+feature156(argument1, argument2, argument3, argument4).output +
+feature157(argument1, argument2, argument3, argument4).output +
+feature158(argument1, argument2, argument3, argument4).output +
+feature159(argument1, argument2, argument3, argument4).output +
+feature160(argument1, argument2, argument3, argument4).output +
+feature161(argument1, argument2, argument3, argument4).output +
+feature162(argument1, argument2, argument3, argument4).output +
+feature163(argument1, argument2, argument3, argument4).output +
+feature164(argument1, argument2, argument3, argument4).output +
+feature165(argument1, argument2, argument3, argument4).output +
+feature166(argument1, argument2, argument3, argument4).output +
+feature167(argument1, argument2, argument3, argument4).output +
+feature168(argument1, argument2, argument3, argument4).output +
+feature169(argument1, argument2, argument3, argument4).output +
+feature170(argument1, argument2, argument3, argument4).output +
+feature171(argument1, argument2, argument3, argument4).output +
+feature172(argument1, argument2, argument3, argument4).output +
+feature173(argument1, argument2, argument3, argument4).output +
+feature174(argument1, argument2, argument3, argument4).output +
+feature175(argument1, argument2, argument3, argument4).output +
+feature176(argument1, argument2, argument3, argument4).output +
+feature177(argument1, argument2, argument3, argument4).output +
+feature178(argument1, argument2, argument3, argument4).output +
+feature179(argument1, argument2, argument3, argument4).output +
+feature180(argument1, argument2, argument3, argument4).output +
+feature181(argument1, argument2, argument3, argument4).output +
+feature182(argument1, argument2, argument3, argument4).output +
+feature183(argument1, argument2, argument3, argument4).output +
+feature184(argument1, argument2, argument3, argument4).output +
+feature185(argument1, argument2, argument3, argument4).output +
+feature186(argument1, argument2, argument3, argument4).output +
+feature187(argument1, argument2, argument3, argument4).output +
+feature188(argument1, argument2, argument3, argument4).output +
+feature189(argument1, argument2, argument3, argument4).output +
+feature190(argument1, argument2, argument3, argument4).output +
+feature191(argument1, argument2, argument3, argument4).output +
+feature192(argument1, argument2, argument3, argument4).output +
+feature193(argument1, argument2, argument3, argument4).output +
+feature194(argument1, argument2, argument3, argument4).output +
+feature195(argument1, argument2, argument3, argument4).output +
+feature196(argument1, argument2, argument3, argument4).output +
+feature197(argument1, argument2, argument3, argument4).output +
+feature198(argument1, argument2, argument3, argument4).output +
+feature199(argument1, argument2, argument3, argument4).output +
+feature200(argument1, argument2, argument3, argument4).output +
+feature201(argument1, argument2, argument3, argument4).output +
+feature202(argument1, argument2, argument3, argument4).output +
+feature203(argument1, argument2, argument3, argument4).output +
+feature204(argument1, argument2, argument3, argument4).output +
+feature205(argument1, argument2, argument3, argument4).output +
+feature206(argument1, argument2, argument3, argument4).output +
+feature207(argument1, argument2, argument3, argument4).output +
+feature208(argument1, argument2, argument3, argument4).output +
+feature209(argument1, argument2, argument3, argument4).output +
+feature210(argument1, argument2, argument3, argument4).output +
+feature211(argument1, argument2, argument3, argument4).output +
+feature212(argument1, argument2, argument3, argument4).output +
+feature213(argument1, argument2, argument3, argument4).output +
+feature214(argument1, argument2, argument3, argument4).output +
+feature215(argument1, argument2, argument3, argument4).output +
+feature216(argument1, argument2, argument3, argument4).output +
+feature217(argument1, argument2, argument3, argument4).output +
+feature218(argument1, argument2, argument3, argument4).output +
+feature219(argument1, argument2, argument3, argument4).output +
+feature220(argument1, argument2, argument3, argument4).output +
+feature221(argument1, argument2, argument3, argument4).output +
+feature222(argument1, argument2, argument3, argument4).output +
+feature223(argument1, argument2, argument3, argument4).output +
+feature224(argument1, argument2, argument3, argument4).output +
+feature225(argument1, argument2, argument3, argument4).output +
+feature226(argument1, argument2, argument3, argument4).output +
+feature227(argument1, argument2, argument3, argument4).output +
+feature228(argument1, argument2, argument3, argument4).output +
+feature229(argument1, argument2, argument3, argument4).output +
+feature230(argument1, argument2, argument3, argument4).output +
+feature231(argument1, argument2, argument3, argument4).output +
+feature232(argument1, argument2, argument3, argument4).output +
+feature233(argument1, argument2, argument3, argument4).output +
+feature234(argument1, argument2, argument3, argument4).output +
+feature235(argument1, argument2, argument3, argument4).output +
+feature236(argument1, argument2, argument3, argument4).output +
+feature237(argument1, argument2, argument3, argument4).output +
+feature238(argument1, argument2, argument3, argument4).output +
+feature239(argument1, argument2, argument3, argument4).output +
+feature240(argument1, argument2, argument3, argument4).output +
+feature241(argument1, argument2, argument3, argument4).output +
+feature242(argument1, argument2, argument3, argument4).output +
+feature243(argument1, argument2, argument3, argument4).output +
+feature244(argument1, argument2, argument3, argument4).output +
+feature245(argument1, argument2, argument3, argument4).output +
+feature246(argument1, argument2, argument3, argument4).output +
+feature247(argument1, argument2, argument3, argument4).output +
+feature248(argument1, argument2, argument3, argument4).output +
+feature249(argument1, argument2, argument3, argument4).output +
+feature250(argument1, argument2, argument3, argument4).output +
+feature251(argument1, argument2, argument3, argument4).output +
+feature252(argument1, argument2, argument3, argument4).output +
+feature253(argument1, argument2, argument3, argument4).output +
+feature254(argument1, argument2, argument3, argument4).output +
+feature255(argument1, argument2, argument3, argument4).output +
+feature256(argument1, argument2, argument3, argument4).output +
+feature257(argument1, argument2, argument3, argument4).output +
+feature258(argument1, argument2, argument3, argument4).output +
+feature259(argument1, argument2, argument3, argument4).output +
+feature260(argument1, argument2, argument3, argument4).output +
+feature261(argument1, argument2, argument3, argument4).output +
+feature262(argument1, argument2, argument3, argument4).output +
+feature263(argument1, argument2, argument3, argument4).output +
+feature264(argument1, argument2, argument3, argument4).output +
+feature265(argument1, argument2, argument3, argument4).output +
+feature266(argument1, argument2, argument3, argument4).output +
+feature267(argument1, argument2, argument3, argument4).output +
+feature268(argument1, argument2, argument3, argument4).output +
+feature269(argument1, argument2, argument3, argument4).output +
+feature270(argument1, argument2, argument3, argument4).output +
+feature271(argument1, argument2, argument3, argument4).output +
+feature272(argument1, argument2, argument3, argument4).output +
+feature273(argument1, argument2, argument3, argument4).output +
+feature274(argument1, argument2, argument3, argument4).output +
+feature275(argument1, argument2, argument3, argument4).output +
+feature276(argument1, argument2, argument3, argument4).output +
+feature277(argument1, argument2, argument3, argument4).output +
+feature278(argument1, argument2, argument3, argument4).output +
+feature279(argument1, argument2, argument3, argument4).output +
+feature280(argument1, argument2, argument3, argument4).output +
+feature281(argument1, argument2, argument3, argument4).output +
+feature282(argument1, argument2, argument3, argument4).output +
+feature283(argument1, argument2, argument3, argument4).output +
+feature284(argument1, argument2, argument3, argument4).output +
+feature285(argument1, argument2, argument3, argument4).output +
+feature286(argument1, argument2, argument3, argument4).output +
+feature287(argument1, argument2, argument3, argument4).output +
+feature288(argument1, argument2, argument3, argument4).output +
+feature289(argument1, argument2, argument3, argument4).output +
+feature290(argument1, argument2, argument3, argument4).output +
+feature291(argument1, argument2, argument3, argument4).output +
+feature292(argument1, argument2, argument3, argument4).output +
+feature293(argument1, argument2, argument3, argument4).output +
+feature294(argument1, argument2, argument3, argument4).output +
+feature295(argument1, argument2, argument3, argument4).output +
+feature296(argument1, argument2, argument3, argument4).output +
+feature297(argument1, argument2, argument3, argument4).output +
+feature298(argument1, argument2, argument3, argument4).output +
+feature299(argument1, argument2, argument3, argument4).output +
+feature300(argument1, argument2, argument3, argument4).output \ No newline at end of file
diff --git a/config-model/src/test/derived/rankexpression/rank-profiles.cfg b/config-model/src/test/derived/rankexpression/rank-profiles.cfg
new file mode 100644
index 00000000000..e890b75770b
--- /dev/null
+++ b/config-model/src/test/derived/rankexpression/rank-profiles.cfg
@@ -0,0 +1,296 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "foo"
+rankprofile[0].fef.property[0].value "bar, baz"
+rankprofile[0].fef.property[1].name "foo"
+rankprofile[0].fef.property[1].value "foobar"
+rankprofile[0].fef.property[2].name "qux"
+rankprofile[0].fef.property[2].value "quux"
+rankprofile[0].fef.property[3].name "foo.bar"
+rankprofile[0].fef.property[3].value "foo.bar"
+rankprofile[0].fef.property[4].name "foo.bar.baz"
+rankprofile[0].fef.property[4].value "123"
+rankprofile[0].fef.property[5].name "foo(bar).baz.2"
+rankprofile[0].fef.property[5].value "123.4"
+rankprofile[0].fef.property[6].name "foo(bar).baz.qux"
+rankprofile[0].fef.property[6].value "foo(bar)"
+rankprofile[0].fef.property[7].name "nud"
+rankprofile[0].fef.property[7].value "ity"
+rankprofile[0].fef.property[8].name "vespa.rank.firstphase"
+rankprofile[0].fef.property[8].value "classicRank"
+rankprofile[0].fef.property[9].name "vespa.rank.secondphase"
+rankprofile[0].fef.property[9].value "rankingExpression(secondphase)"
+rankprofile[0].fef.property[10].name "rankingExpression(secondphase).rankingScript"
+rankprofile[0].fef.property[10].value "4"
+rankprofile[0].fef.property[11].name "vespa.dump.feature"
+rankprofile[0].fef.property[11].value "attribute(foo1).out"
+rankprofile[0].fef.property[12].name "vespa.dump.feature"
+rankprofile[0].fef.property[12].value "attribute(bar1.out)"
+rankprofile[0].fef.property[13].name "vespa.dump.feature"
+rankprofile[0].fef.property[13].value "attribute(foo2).out"
+rankprofile[0].fef.property[14].name "vespa.dump.feature"
+rankprofile[0].fef.property[14].value "attribute(bar2).out"
+rankprofile[0].fef.property[15].name "vespa.dump.feature"
+rankprofile[0].fef.property[15].value "attribute(foo3).out"
+rankprofile[0].fef.property[16].name "vespa.dump.feature"
+rankprofile[0].fef.property[16].value "attribute(bar3).out"
+rankprofile[0].fef.property[17].name "vespa.dump.feature"
+rankprofile[0].fef.property[17].value "attribute(foo4).out"
+rankprofile[0].fef.property[18].name "vespa.dump.feature"
+rankprofile[0].fef.property[18].value "attribute(bar4).out"
+rankprofile[0].fef.property[19].name "vespa.hitcollector.heapsize"
+rankprofile[0].fef.property[19].value "10"
+rankprofile[0].fef.property[20].name "vespa.hitcollector.arraysize"
+rankprofile[0].fef.property[20].value "20"
+rankprofile[0].fef.property[21].name "vespa.hitcollector.rankscoredroplimit"
+rankprofile[0].fef.property[21].value "-0.5"
+rankprofile[0].fef.property[22].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[0].fef.property[22].value "true"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[2].name "static"
+rankprofile[2].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[2].fef.property[0].value "attribute"
+rankprofile[2].fef.property[1].name "vespa.rank.secondphase"
+rankprofile[2].fef.property[1].value "rankingExpression(secondphase)"
+rankprofile[2].fef.property[2].name "rankingExpression(secondphase).rankingScript"
+rankprofile[2].fef.property[2].value "10 + feature(arg1).out.out"
+rankprofile[2].fef.property[3].name "vespa.summary.feature"
+rankprofile[2].fef.property[3].value "attribute(foo1).out"
+rankprofile[2].fef.property[4].name "vespa.summary.feature"
+rankprofile[2].fef.property[4].value "attribute(bar1.out)"
+rankprofile[2].fef.property[5].name "vespa.summary.feature"
+rankprofile[2].fef.property[5].value "attribute(foo2).out"
+rankprofile[2].fef.property[6].name "vespa.summary.feature"
+rankprofile[2].fef.property[6].value "attribute(bar2).out"
+rankprofile[2].fef.property[7].name "vespa.summary.feature"
+rankprofile[2].fef.property[7].value "attribute(foo3).out"
+rankprofile[2].fef.property[8].name "vespa.summary.feature"
+rankprofile[2].fef.property[8].value "attribute(bar3).out"
+rankprofile[2].fef.property[9].name "vespa.summary.feature"
+rankprofile[2].fef.property[9].value "attribute(foo4).out"
+rankprofile[2].fef.property[10].name "vespa.summary.feature"
+rankprofile[2].fef.property[10].value "attribute(bar4).out"
+rankprofile[3].name "overflow"
+rankprofile[3].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[3].fef.property[0].value "rankingExpression(firstphase)"
+rankprofile[3].fef.property[1].name "rankingExpression(firstphase).rankingScript"
+rankprofile[3].fef.property[1].value "feature1(argument1,argument2,argument3,argument4).output + feature2(argument1,argument2,argument3,argument4).output + feature3(argument1,argument2,argument3,argument4).output + feature4(argument1,argument2,argument3,argument4).output + feature5(argument1,argument2,argument3,argument4).output + feature6(argument1,argument2,argument3,argument4).output + feature7(argument1,argument2,argument3,argument4).output + feature8(argument1,argument2,argument3,argument4).output + feature9(argument1,argument2,argument3,argument4).output + feature10(argument1,argument2,argument3,argument4).output + feature11(argument1,argument2,argument3,argument4).output + feature12(argument1,argument2,argument3,argument4).output + feature13(argument1,argument2,argument3,argument4).output + feature14(argument1,argument2,argument3,argument4).output + feature15(argument1,argument2,argument3,argument4).output + feature16(argument1,argument2,argument3,argument4).output + feature17(argument1,argument2,argument3,argument4).output + feature18(argument1,argument2,argument3,argument4).output + feature19(argument1,argument2,argument3,argument4).output + feature20(argument1,argument2,argument3,argument4).output + feature21(argument1,argument2,argument3,argument4).output + feature22(argument1,argument2,argument3,argument4).output + feature23(argument1,argument2,argument3,argument4).output + feature24(argument1,argument2,argument3,argument4).output + feature25(argument1,argument2,argument3,argument4).output + feature26(argument1,argument2,argument3,argument4).output + feature27(argument1,argument2,argument3,argument4).output + feature28(argument1,argument2,argument3,argument4).output + feature29(argument1,argument2,argument3,argument4).output + feature30(argument1,argument2,argument3,argument4).output + feature31(argument1,argument2,argument3,argument4).output + feature32(argument1,argument2,argument3,argument4).output + feature33(argument1,argument2,argument3,argument4).output + feature34(argument1,argument2,argument3,argument4).output + feature35(argument1,argument2,argument3,argument4).output + feature36(argument1,argument2,argument3,argument4).output + feature37(argument1,argument2,argument3,argument4).output + feature38(argument1,argument2,argument3,argument4).output + feature39(argument1,argument2,argument3,argument4).output + feature40(argument1,argument2,argument3,argument4).output + feature41(argument1,argument2,argument3,argument4).output + feature42(argument1,argument2,argument3,argument4).output + feature43(argument1,argument2,argument3,argument4).output + feature44(argument1,argument2,argument3,argument4).output + feature45(argument1,argument2,argument3,argument4).output + feature46(argument1,argument2,argument3,argument4).output + feature47(argument1,argument2,argument3,argument4).output + feature48(argument1,argument2,argument3,argument4).output + feature49(argument1,argument2,argument3,argument4).output + feature50(argument1,argument2,argument3,argument4).output + feature51(argument1,argument2,argument3,argument4).output + feature52(argument1,argument2,argument3,argument4).output + feature53(argument1,argument2,argument3,argument4).output + feature54(argument1,argument2,argument3,argument4).output + feature55(argument1,argument2,argument3,argument4).output + feature56(argument1,argument2,argument3,argument4).output + feature57(argument1,argument2,argument3,argument4).output + feature58(argument1,argument2,argument3,argument4).output + feature59(argument1,argument2,argument3,argument4).output + feature60(argument1,argument2,argument3,argument4).output + feature61(argument1,argument2,argument3,argument4).output + feature62(argument1,argument2,argument3,argument4).output + feature63(argument1,argument2,argument3,argument4).output + feature64(argument1,argument2,argument3,argument4).output + feature65(argument1,argument2,argument3,argument4).output + feature66(argument1,argument2,argument3,argument4).output + feature67(argument1,argument2,argument3,argument4).output + feature68(argument1,argument2,argument3,argument4).output + feature69(argument1,argument2,argument3,argument4).output + feature70(argument1,argument2,argument3,argument4).output + feature71(argument1,argument2,argument3,argument4).output + feature72(argument1,argument2,argument3,argument4).output + feature73(argument1,argument2,argument3,argument4).output + feature74(argument1,argument2,argument3,argument4).output + feature75(argument1,argument2,argument3,argument4).output + feature76(argument1,argument2,argument3,argument4).output + feature77(argument1,argument2,argument3,argument4).output + feature78(argument1,argument2,argument3,argument4).output + feature79(argument1,argument2,argument3,argument4).output + feature80(argument1,argument2,argument3,argument4).output + feature81(argument1,argument2,argument3,argument4).output + feature82(argument1,argument2,argument3,argument4).output + feature83(argument1,argument2,argument3,argument4).output + feature84(argument1,argument2,argument3,argument4).output + feature85(argument1,argument2,argument3,argument4).output + feature86(argument1,argument2,argument3,argument4).output + feature87(argument1,argument2,argument3,argument4).output + feature88(argument1,argument2,argument3,argument4).output + feature89(argument1,argument2,argument3,argument4).output + feature90(argument1,argument2,argument3,argument4).output + feature91(argument1,argument2,argument3,argument4).output + feature92(argument1,argument2,argument3,argument4).output + feature93(argument1,argument2,argument3,argument4).output + feature94(argument1,argument2,argument3,argument4).output + feature95(argument1,argument2,argument3,argument4).output + feature96(argument1,argument2,argument3,argument4).output + feature97(argument1,argument2,argument3,argument4).output + feature98(argument1,argument2,argument3,argument4).output + feature99(argument1,argument2,argument3,argument4).output + feature100(argument1,argument2,argument3,argument4).output + feature101(argument1,argument2,argument3,argument4).output + feature102(argument1,argument2,argument3,argument4).output + feature103(argument1,argument2,argument3,argument4).output + feature104(argument1,argument2,argument3,argument4).output + feature105(argument1,argument2,argument3,argument4).output + feature106(argument1,argument2,argument3,argument4).output + feature107(argument1,argument2,argument3,argument4).output + feature108(argument1,argument2,argument3,argument4).output + feature109(argument1,argument2,argument3,argument4).output + feature110(argument1,argument2,argument3,argument4).output + feature111(argument1,argument2,argument3,argument4).output + feature112(argument1,argument2,argument3,argument4).output + feature113(argument1,argument2,argument3,argument4).output + feature114(argument1,argument2,argument3,argument4).output + feature115(argument1,argument2,argument3,argument4).output + feature116(argument1,argument2,argument3,argument4).output + feature117(argument1,argument2,argument3,argument4).output + feature118(argument1,argument2,argument3,argument4).output + feature119(argument1,argument2,argument3,argument4).output + feature120(argument1,argument2,argument3,argument4).output + feature121(argument1,argument2,argument3,argument4).output + feature122(argument1,argument2,argument3,argument4).output + feature123(argument1,argument2,argument3,argument4).output + feature124(argument1,argument2,argument3,argument4).output + feature125(argument1,argument2,argument3,argument4).output + feature126(argument1,argument2,argument3,argument4).output + feature127(argument1,argument2,argument3,argument4).output + feature128(argument1,argument2,argument3,argument4).output + feature129(argument1,argument2,argument3,argument4).output + feature130(argument1,argument2,argument3,argument4).output + feature131(argument1,argument2,argument3,argument4).output + feature132(argument1,argument2,argument3,argument4).output + feature133(argument1,argument2,argument3,argument4).output + feature134(argument1,argument2,argument3,argument4).output + feature135(argument1,argument2,argument3,argument4).output + feature136(argument1,argument2,argument3,argument4).output + feature137(argument1,argument2,argument3,argument4).output + feature138(argument1,argument2,argument3,argument4).output + feature139(argument1,argument2,argument3,argument4).output + feature140(argument1,argument2,argument3,argument4).output + feature141(argument1,argument2,argument3,argument4).output + feature142(argument1,argument2,argument3,argument4).output + feature143(argument1,argument2,argument3,argument4).output + feature144(argument1,argument2,argument3,argument4).output + feature145(argument1,argument2,argument3,argument4).output + feature146(argument1,argument2,argument3,argument4).output + feature147(argument1,argument2,argument3,argument4).output + feature148(argument1,argument2,argument3,argument4).output + feature149(argument1,argument2,argument3,argument4).output + feature150(argument1,argument2,argument3,argument4).output + feature151(argument1,argument2,argument3,argument4).output + feature152(argument1,argument2,argument3,argument4).output + feature153(argument1,argument2,argument3,argument4).output + feature154(argument1,argument2,argument3,argument4).output + feature155(argument1,argument2,argument3,argument4).output + feature156(argument1,argument2,argument3,argument4).output + feature157(argument1,argument2,argument3,argument4).output + feature158(argument1,argument2,argument3,argument4).output + feature159(argument1,argument2,argument3,argument4).output + feature160(argument1,argument2,argument3,argument4).output + feature161(argument1,argument2,argument3,argument4).output + feature162(argument1,argument2,argument3,argument4).output + feature163(argument1,argument2,argument3,argument4).output + feature164(argument1,argument2,argument3,argument4).output + feature165(argument1,argument2,argument3,argument4).output + feature166(argument1,argument2,argument3,argument4).output + feature167(argument1,argument2,argument3,argument4).output + feature168(argument1,argument2,argument3,argument4).output + feature169(argument1,argument2,argument3,argument4).output + feature170(argument1,argument2,argument3,argument4).output + feature171(argument1,argument2,argument3,argument4).output + feature172(argument1,argument2,argument3,argument4).output + feature173(argument1,argument2,argument3,argument4).output + feature174(argument1,argument2,argument3,argument4).output + feature175(argument1,argument2,argument3,argument4).output + feature176(argument1,argument2,argument3,argument4).output + feature177(argument1,argument2,argument3,argument4).output + feature178(argument1,argument2,argument3,argument4).output + feature179(argument1,argument2,argument3,argument4).output + feature180(argument1,argument2,argument3,argument4).output + feature181(argument1,argument2,argument3,argument4).output + feature182(argument1,argument2,argument3,argument4).output + feature183(argument1,argument2,argument3,argument4).output + feature184(argument1,argument2,argument3,argument4).output + feature185(argument1,argument2,argument3,argument4).output + feature186(argument1,argument2,argument3,argument4).output + feature187(argument1,argument2,argument3,argument4).output + feature188(argument1,argument2,argument3,argument4).output + feature189(argument1,argument2,argument3,argument4).output + feature190(argument1,argument2,argument3,argument4).output + feature191(argument1,argument2,argument3,argument4).output + feature192(argument1,argument2,argument3,argument4).output + feature193(argument1,argument2,argument3,argument4).output + feature194(argument1,argument2,argument3,argument4).output + feature195(argument1,argument2,argument3,argument4).output + feature196(argument1,argument2,argument3,argument4).output + feature197(argument1,argument2,argument3,argument4).output + feature198(argument1,argument2,argument3,argument4).output + feature199(argument1,argument2,argument3,argument4).output + feature200(argument1,argument2,argument3,argument4).output + feature201(argument1,argument2,argument3,argument4).output + feature202(argument1,argument2,argument3,argument4).output + feature203(argument1,argument2,argument3,argument4).output + feature204(argument1,argument2,argument3,argument4).output + feature205(argument1,argument2,argument3,argument4).output + feature206(argument1,argument2,argument3,argument4).output + feature207(argument1,argument2,argument3,argument4).output + feature208(argument1,argument2,argument3,argument4).output + feature209(argument1,argument2,argument3,argument4).output + feature210(argument1,argument2,argument3,argument4).output + feature211(argument1,argument2,argument3,argument4).output + feature212(argument1,argument2,argument3,argument4).output + feature213(argument1,argument2,argument3,argument4).output + feature214(argument1,argument2,argument3,argument4).output + feature215(argument1,argument2,argument3,argument4).output + feature216(argument1,argument2,argument3,argument4).output + feature217(argument1,argument2,argument3,argument4).output + feature218(argument1,argument2,argument3,argument4).output + feature219(argument1,argument2,argument3,argument4).output + feature220(argument1,argument2,argument3,argument4).output + feature221(argument1,argument2,argument3,argument4).output + feature222(argument1,argument2,argument3,argument4).output + feature223(argument1,argument2,argument3,argument4).output + feature224(argument1,argument2,argument3,argument4).output + feature225(argument1,argument2,argument3,argument4).output + feature226(argument1,argument2,argument3,argument4).output + feature227(argument1,argument2,argument3,argument4).output + feature228(argument1,argument2,argument3,argument4).output + feature229(argument1,argument2,argument3,argument4).output + feature230(argument1,argument2,argument3,argument4).output + feature231(argument1,argument2,argument3,argument4).output + feature232(argument1,argument2,argument3,argument4).output + feature233(argument1,argument2,argument3,argument4).output + feature234(argument1,argument2,argument3,argument4).output + feature235(argument1,argument2,argument3,argument4).output + feature236(argument1,argument2,argument3,argument4).output + feature237(argument1,argument2,argument3,argument4).output + feature238(argument1,argument2,argument3,argument4).output + feature239(argument1,argument2,argument3,argument4).output + feature240(argument1,argument2,argument3,argument4).output + feature241(argument1,argument2,argument3,argument4).output + feature242(argument1,argument2,argument3,argument4).output + feature243(argument1,argument2,argument3,argument4).output + feature244(argument1,argument2,argument3,argument4).output + feature245(argument1,argument2,argument3,argument4).output + feature246(argument1,argument2,argument3,argument4).output + feature247(argument1,argument2,argument3,argument4).output + feature248(argument1,argument2,argument3,argument4).output + feature249(argument1,argument2,argument3,argument4).output + feature250(argument1,argument2,argument3,argument4).output + feature251(argument1,argument2,argument3,argument4).output + feature252(argument1,argument2,argument3,argument4).output + feature253(argument1,argument2,argument3,argument4).output + feature254(argument1,argument2,argument3,argument4).output + feature255(argument1,argument2,argument3,argument4).output + feature256(argument1,argument2,argument3,argument4).output + feature257(argument1,argument2,argument3,argument4).output + feature258(argument1,argument2,argument3,argument4).output + feature259(argument1,argument2,argument3,argument4).output + feature260(argument1,argument2,argument3,argument4).output + feature261(argument1,argument2,argument3,argument4).output + feature262(argument1,argument2,argument3,argument4).output + feature263(argument1,argument2,argument3,argument4).output + feature264(argument1,argument2,argument3,argument4).output + feature265(argument1,argument2,argument3,argument4).output + feature266(argument1,argument2,argument3,argument4).output + feature267(argument1,argument2,argument3,argument4).output + feature268(argument1,argument2,argument3,argument4).output + feature269(argument1,argument2,argument3,argument4).output + feature270(argument1,argument2,argument3,argument4).output + feature271(argument1,argument2,argument3,argument4).output + feature272(argument1,argument2,argument3,argument4).output + feature273(argument1,argument2,argument3,argument4).output + feature274(argument1,argument2,argument3,argument4).output + feature275(argument1,argument2,argument3,argument4).output + feature276(argument1,argument2,argument3,argument4).output + feature277(argument1,argument2,argument3,argument4).output + feature278(argument1,argument2,argument3,argument4).output + feature279(argument1,argument2,argument3,argument4).output + feature280(argument1,argument2,argument3,argument4).output + feature281(argument1,argument2,argument3,argument4).output + feature282(argument1,argument2,argument3,argument4).output + feature283(argument1,argument2,argument3,argument4).output + feature284(argument1,argument2,argument3,argument4).output + feature285(argument1,argument2,argument3,argument4).output + feature286(argument1,argument2,argument3,argument4).output + feature287(argument1,argument2,argument3,argument4).output + feature288(argument1,argument2,argument3,argument4).output + feature289(argument1,argument2,argument3,argument4).output + feature290(argument1,argument2,argument3,argument4).output + feature291(argument1,argument2,argument3,argument4).output + feature292(argument1,argument2,argument3,argument4).output + feature293(argument1,argument2,argument3,argument4).output + feature294(argument1,argument2,argument3,argument4).output + feature295(argument1,argument2,argument3,argument4).output + feature296(argument1,argument2,argument3,argument4).output + feature297(argument1,argument2,argument3,argument4).output + feature298(argument1,argument2,argument3,argument4).output + feature299(argument1,argument2,argument3,argument4).output + feature300(argument1,argument2,argument3,argument4).output"
+rankprofile[3].fef.property[2].name "vespa.rank.secondphase"
+rankprofile[3].fef.property[2].value "rankingExpression(secondphase)"
+rankprofile[3].fef.property[3].name "rankingExpression(secondphase).rankingScript"
+rankprofile[3].fef.property[3].value "exp(0) + mysum(attribute(foo),\"attribute( bar )\",\"attribute( \\\"baz\\\" )\")"
+rankprofile[3].fef.property[4].name "vespa.hitcollector.heapsize"
+rankprofile[3].fef.property[4].value "101"
+rankprofile[3].fef.property[5].name "vespa.hitcollector.arraysize"
+rankprofile[3].fef.property[5].value "201"
+rankprofile[3].fef.property[6].name "vespa.hitcollector.rankscoredroplimit"
+rankprofile[3].fef.property[6].value "501.5"
+rankprofile[4].name "duplicates"
+rankprofile[4].fef.property[0].name "fieldMatch(a).proximityLimit"
+rankprofile[4].fef.property[0].value "4"
+rankprofile[4].fef.property[1].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[1].value "0.2"
+rankprofile[4].fef.property[2].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[2].value "0.4"
+rankprofile[4].fef.property[3].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[3].value "0.6"
+rankprofile[4].fef.property[4].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[4].value "0.8"
+rankprofile[4].fef.property[5].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[5].value "1"
+rankprofile[4].fef.property[6].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[6].value "0.8"
+rankprofile[4].fef.property[7].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[7].value "0.6"
+rankprofile[4].fef.property[8].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[8].value "0.4"
+rankprofile[4].fef.property[9].name "fieldMatch(a).proximityTable"
+rankprofile[4].fef.property[9].value "0.2"
+rankprofile[5].name "whitespace1"
+rankprofile[5].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[5].fef.property[0].value "rankingExpression(firstphase)"
+rankprofile[5].fef.property[1].name "rankingExpression(firstphase).rankingScript"
+rankprofile[5].fef.property[1].value "1"
+rankprofile[6].name "whitespace2"
+rankprofile[6].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[6].fef.property[0].value "rankingExpression(firstphase)"
+rankprofile[6].fef.property[1].name "rankingExpression(firstphase).rankingScript"
+rankprofile[6].fef.property[1].value "1"
+rankprofile[7].name "macros"
+rankprofile[7].fef.property[0].name "rankingExpression(fourtimessum).rankingScript"
+rankprofile[7].fef.property[0].value "4 * (var1 + var2)"
+rankprofile[7].fef.property[1].name "rankingExpression(myfeature).rankingScript"
+rankprofile[7].fef.property[1].value "70 * fieldMatch(title).completeness * pow(0 - fieldMatch(title).earliness,2) + 30 * pow(0 - fieldMatch(description).earliness,2)"
+rankprofile[7].fef.property[2].name "rankingExpression(fourtimessum@5cf279212355b980.67f1e87166cfef86).rankingScript"
+rankprofile[7].fef.property[2].value "4 * (match + rankBoost)"
+rankprofile[7].fef.property[3].name "vespa.rank.firstphase"
+rankprofile[7].fef.property[3].value "rankingExpression(firstphase)"
+rankprofile[7].fef.property[4].name "rankingExpression(firstphase).rankingScript"
+rankprofile[7].fef.property[4].value "match + fieldMatch(title) + rankingExpression(myfeature)"
+rankprofile[7].fef.property[5].name "vespa.rank.secondphase"
+rankprofile[7].fef.property[5].value "rankingExpression(fourtimessum@5cf279212355b980.67f1e87166cfef86)"
+rankprofile[7].fef.property[6].name "vespa.summary.feature"
+rankprofile[7].fef.property[6].value "fieldMatch(title)"
+rankprofile[8].name "macros2"
+rankprofile[8].fef.property[0].name "foo"
+rankprofile[8].fef.property[0].value "some, list"
+rankprofile[8].fef.property[1].name "rankingExpression(fourtimessum).rankingScript"
+rankprofile[8].fef.property[1].value "4 * (var1 + var2)"
+rankprofile[8].fef.property[2].name "rankingExpression(myfeature).rankingScript"
+rankprofile[8].fef.property[2].value "70 * fieldMatch(title).completeness * pow(0 - fieldMatch(title).earliness,2) + 30 * pow(0 - fieldMatch(description).earliness,2)"
+rankprofile[8].fef.property[3].name "rankingExpression(mysummaryfeature).rankingScript"
+rankprofile[8].fef.property[3].value "70 * fieldMatch(title).completeness"
+rankprofile[8].fef.property[4].name "rankingExpression(mysummaryfeature2).rankingScript"
+rankprofile[8].fef.property[4].value "71 * fieldMatch(title).completeness"
+rankprofile[8].fef.property[5].name "rankingExpression(fourtimessum@2b1138e8965e7ff5.67f1e87166cfef86).rankingScript"
+rankprofile[8].fef.property[5].value "4 * (match + match)"
+rankprofile[8].fef.property[6].name "vespa.rank.firstphase"
+rankprofile[8].fef.property[6].value "classicRank"
+rankprofile[8].fef.property[7].name "vespa.rank.secondphase"
+rankprofile[8].fef.property[7].value "rankingExpression(secondphase)"
+rankprofile[8].fef.property[8].name "rankingExpression(secondphase).rankingScript"
+rankprofile[8].fef.property[8].value "rankingExpression(fourtimessum@2b1138e8965e7ff5.67f1e87166cfef86) + rankingExpression(mysummaryfeature) + rankingExpression(myfeature)"
+rankprofile[8].fef.property[9].name "vespa.summary.feature"
+rankprofile[8].fef.property[9].value "rankingExpression(mysummaryfeature2)"
+rankprofile[8].fef.property[10].name "vespa.summary.feature"
+rankprofile[8].fef.property[10].value "rankingExpression(mysummaryfeature)"
+rankprofile[9].name "macros3"
+rankprofile[9].fef.property[0].name "rankingExpression(onlyusedinsummaryfeature).rankingScript"
+rankprofile[9].fef.property[0].value "5"
+rankprofile[9].fef.property[1].name "vespa.summary.feature"
+rankprofile[9].fef.property[1].value "rankingExpression(matches(title,rankingExpression(onlyusedinsummaryfeature)))"
+rankprofile[10].name "macros3-inherited"
+rankprofile[10].fef.property[0].name "rankingExpression(onlyusedinsummaryfeature).rankingScript"
+rankprofile[10].fef.property[0].value "5"
+rankprofile[10].fef.property[1].name "vespa.summary.feature"
+rankprofile[10].fef.property[1].value "rankingExpression(matches(title,rankingExpression(onlyusedinsummaryfeature)))"
+rankprofile[11].name "macros-inherited"
+rankprofile[11].fef.property[0].name "foo"
+rankprofile[11].fef.property[0].value "some, list"
+rankprofile[11].fef.property[1].name "rankingExpression(fourtimessum).rankingScript"
+rankprofile[11].fef.property[1].value "4 * (var1 + var2)"
+rankprofile[11].fef.property[2].name "rankingExpression(myfeature).rankingScript"
+rankprofile[11].fef.property[2].value "70 * fieldMatch(title).completeness * pow(0 - fieldMatch(title).earliness,2) + 30 * pow(0 - fieldMatch(description).earliness,2)"
+rankprofile[11].fef.property[3].name "rankingExpression(mysummaryfeature).rankingScript"
+rankprofile[11].fef.property[3].value "80 * fieldMatch(title).completeness"
+rankprofile[11].fef.property[4].name "rankingExpression(mysummaryfeature2).rankingScript"
+rankprofile[11].fef.property[4].value "71 * fieldMatch(title).completeness"
+rankprofile[11].fef.property[5].name "rankingExpression(fourtimessum@2b1138e8965e7ff5.67f1e87166cfef86).rankingScript"
+rankprofile[11].fef.property[5].value "4 * (match + match)"
+rankprofile[11].fef.property[6].name "vespa.rank.firstphase"
+rankprofile[11].fef.property[6].value "rankingExpression(firstphase)"
+rankprofile[11].fef.property[7].name "rankingExpression(firstphase).rankingScript"
+rankprofile[11].fef.property[7].value "20000 * rankingExpression(myfeature) + rankingExpression(mysummaryfeature)"
+rankprofile[11].fef.property[8].name "vespa.rank.secondphase"
+rankprofile[11].fef.property[8].value "rankingExpression(secondphase)"
+rankprofile[11].fef.property[9].name "rankingExpression(secondphase).rankingScript"
+rankprofile[11].fef.property[9].value "rankingExpression(fourtimessum@2b1138e8965e7ff5.67f1e87166cfef86) + rankingExpression(mysummaryfeature) + rankingExpression(myfeature)"
+rankprofile[11].fef.property[10].name "vespa.summary.feature"
+rankprofile[11].fef.property[10].value "rankingExpression(mysummaryfeature2)"
+rankprofile[11].fef.property[11].name "vespa.summary.feature"
+rankprofile[11].fef.property[11].value "rankingExpression(mysummaryfeature)"
+rankprofile[12].name "macros-inherited2"
+rankprofile[12].fef.property[0].name "foo"
+rankprofile[12].fef.property[0].value "some, list"
+rankprofile[12].fef.property[1].name "rankingExpression(fourtimessum).rankingScript"
+rankprofile[12].fef.property[1].value "4 * (var1 + var2)"
+rankprofile[12].fef.property[2].name "rankingExpression(myfeature).rankingScript"
+rankprofile[12].fef.property[2].value "70 * fieldMatch(title).completeness * pow(0 - fieldMatch(title).earliness,2) + 30 * pow(0 - fieldMatch(description).earliness,2)"
+rankprofile[12].fef.property[3].name "rankingExpression(mysummaryfeature).rankingScript"
+rankprofile[12].fef.property[3].value "80 * fieldMatch(title).completeness"
+rankprofile[12].fef.property[4].name "rankingExpression(mysummaryfeature2).rankingScript"
+rankprofile[12].fef.property[4].value "71 * fieldMatch(title).completeness"
+rankprofile[12].fef.property[5].name "rankingExpression(fourtimessum@2b1138e8965e7ff5.67f1e87166cfef86).rankingScript"
+rankprofile[12].fef.property[5].value "4 * (match + match)"
+rankprofile[12].fef.property[6].name "vespa.rank.firstphase"
+rankprofile[12].fef.property[6].value "rankingExpression(firstphase)"
+rankprofile[12].fef.property[7].name "rankingExpression(firstphase).rankingScript"
+rankprofile[12].fef.property[7].value "30000 * rankingExpression(mysummaryfeature) + rankingExpression(myfeature)"
+rankprofile[12].fef.property[8].name "vespa.rank.secondphase"
+rankprofile[12].fef.property[8].value "rankingExpression(secondphase)"
+rankprofile[12].fef.property[9].name "rankingExpression(secondphase).rankingScript"
+rankprofile[12].fef.property[9].value "rankingExpression(fourtimessum@2b1138e8965e7ff5.67f1e87166cfef86) + rankingExpression(mysummaryfeature) + rankingExpression(myfeature)"
+rankprofile[12].fef.property[10].name "vespa.summary.feature"
+rankprofile[12].fef.property[10].value "rankingExpression(mysummaryfeature2)"
+rankprofile[12].fef.property[11].name "vespa.summary.feature"
+rankprofile[12].fef.property[11].value "rankingExpression(mysummaryfeature)"
+rankprofile[13].name "macros-inherited3"
+rankprofile[13].fef.property[0].name "foo"
+rankprofile[13].fef.property[0].value "some, list"
+rankprofile[13].fef.property[1].name "rankingExpression(fourtimessum).rankingScript"
+rankprofile[13].fef.property[1].value "4 * (var1 + var2)"
+rankprofile[13].fef.property[2].name "rankingExpression(myfeature).rankingScript"
+rankprofile[13].fef.property[2].value "700 * fieldMatch(title).completeness"
+rankprofile[13].fef.property[3].name "rankingExpression(mysummaryfeature).rankingScript"
+rankprofile[13].fef.property[3].value "80 * fieldMatch(title).completeness"
+rankprofile[13].fef.property[4].name "rankingExpression(mysummaryfeature2).rankingScript"
+rankprofile[13].fef.property[4].value "71 * fieldMatch(title).completeness"
+rankprofile[13].fef.property[5].name "vespa.rank.firstphase"
+rankprofile[13].fef.property[5].value "rankingExpression(firstphase)"
+rankprofile[13].fef.property[6].name "rankingExpression(firstphase).rankingScript"
+rankprofile[13].fef.property[6].value "30000 * rankingExpression(mysummaryfeature) + rankingExpression(myfeature)"
+rankprofile[13].fef.property[7].name "vespa.rank.secondphase"
+rankprofile[13].fef.property[7].value "rankingExpression(secondphase)"
+rankprofile[13].fef.property[8].name "rankingExpression(secondphase).rankingScript"
+rankprofile[13].fef.property[8].value "40000 * rankingExpression(mysummaryfeature) + rankingExpression(myfeature)"
+rankprofile[13].fef.property[9].name "vespa.summary.feature"
+rankprofile[13].fef.property[9].value "rankingExpression(mysummaryfeature2)"
+rankprofile[13].fef.property[10].name "vespa.summary.feature"
+rankprofile[13].fef.property[10].value "rankingExpression(mysummaryfeature)"
+rankprofile[14].name "macros-refering-macros"
+rankprofile[14].fef.property[0].name "rankingExpression(m1).rankingScript"
+rankprofile[14].fef.property[0].value "700 * fieldMatch(title).completeness"
+rankprofile[14].fef.property[1].name "rankingExpression(m2).rankingScript"
+rankprofile[14].fef.property[1].value "rankingExpression(m1) * 67"
+rankprofile[14].fef.property[2].name "rankingExpression(m4).rankingScript"
+rankprofile[14].fef.property[2].value "703 * fieldMatch(fromfile).completeness"
+rankprofile[14].fef.property[3].name "vespa.rank.secondphase"
+rankprofile[14].fef.property[3].value "rankingExpression(secondphase)"
+rankprofile[14].fef.property[4].name "rankingExpression(secondphase).rankingScript"
+rankprofile[14].fef.property[4].value "40000 * rankingExpression(m2)"
+rankprofile[15].name "macros-refering-macros-inherited"
+rankprofile[15].fef.property[0].name "rankingExpression(m1).rankingScript"
+rankprofile[15].fef.property[0].value "700 * fieldMatch(title).completeness"
+rankprofile[15].fef.property[1].name "rankingExpression(m2).rankingScript"
+rankprofile[15].fef.property[1].value "rankingExpression(m1) * 67"
+rankprofile[15].fef.property[2].name "rankingExpression(m4).rankingScript"
+rankprofile[15].fef.property[2].value "701 * fieldMatch(title).completeness"
+rankprofile[15].fef.property[3].name "rankingExpression(m3).rankingScript"
+rankprofile[15].fef.property[3].value "if (isNan(attribute(nrtgmp)) == 1, 0.0, rankingExpression(m2))"
+rankprofile[15].fef.property[4].name "vespa.rank.secondphase"
+rankprofile[15].fef.property[4].value "rankingExpression(secondphase)"
+rankprofile[15].fef.property[5].name "rankingExpression(secondphase).rankingScript"
+rankprofile[15].fef.property[5].value "3000 * rankingExpression(m2)"
+rankprofile[16].name "macros-refering-macros-inherited2"
+rankprofile[16].fef.property[0].name "rankingExpression(m1).rankingScript"
+rankprofile[16].fef.property[0].value "700 * fieldMatch(title).completeness"
+rankprofile[16].fef.property[1].name "rankingExpression(m2).rankingScript"
+rankprofile[16].fef.property[1].value "rankingExpression(m1) * 67"
+rankprofile[16].fef.property[2].name "rankingExpression(m4).rankingScript"
+rankprofile[16].fef.property[2].value "703 * fieldMatch(fromfile).completeness"
+rankprofile[16].fef.property[3].name "vespa.rank.secondphase"
+rankprofile[16].fef.property[3].value "rankingExpression(secondphase)"
+rankprofile[16].fef.property[4].name "rankingExpression(secondphase).rankingScript"
+rankprofile[16].fef.property[4].value "3002 * rankingExpression(m2)"
+rankprofile[17].name "macros-refering-macros-inherited-two-levels"
+rankprofile[17].fef.property[0].name "rankingExpression(m1).rankingScript"
+rankprofile[17].fef.property[0].value "700 * fieldMatch(title).completeness"
+rankprofile[17].fef.property[1].name "rankingExpression(m2).rankingScript"
+rankprofile[17].fef.property[1].value "rankingExpression(m1) * 67"
+rankprofile[17].fef.property[2].name "rankingExpression(m4).rankingScript"
+rankprofile[17].fef.property[2].value "701 * fieldMatch(title).completeness"
+rankprofile[17].fef.property[3].name "rankingExpression(m3).rankingScript"
+rankprofile[17].fef.property[3].value "if (isNan(attribute(nrtgmp)) == 1, 0.0, rankingExpression(m2))"
+rankprofile[17].fef.property[4].name "rankingExpression(m5).rankingScript"
+rankprofile[17].fef.property[4].value "if (isNan(attribute(glmpfw)) == 1, rankingExpression(m1), rankingExpression(m4))"
+rankprofile[17].fef.property[5].name "vespa.rank.secondphase"
+rankprofile[17].fef.property[5].value "rankingExpression(secondphase)"
+rankprofile[17].fef.property[6].name "rankingExpression(secondphase).rankingScript"
+rankprofile[17].fef.property[6].value "3000 * rankingExpression(m2)" \ No newline at end of file
diff --git a/config-model/src/test/derived/rankexpression/rankexpression.expression b/config-model/src/test/derived/rankexpression/rankexpression.expression
new file mode 100644
index 00000000000..d6cb73c829d
--- /dev/null
+++ b/config-model/src/test/derived/rankexpression/rankexpression.expression
@@ -0,0 +1 @@
+if ( 3 > 2 , 10 , 24 ) + feature ( arg1 ) . out.out
diff --git a/config-model/src/test/derived/rankexpression/rankexpression.sd b/config-model/src/test/derived/rankexpression/rankexpression.sd
new file mode 100644
index 00000000000..139615bbd01
--- /dev/null
+++ b/config-model/src/test/derived/rankexpression/rankexpression.sd
@@ -0,0 +1,297 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search rankexpression {
+
+ document rankexpression {
+
+ field artist type string {
+ indexing: summary | index
+ # index-to: artist, default
+ }
+
+ field title type string {
+ indexing: summary | index
+ # index-to: title, default
+ }
+
+ field surl type string {
+ indexing: summary
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ }
+
+ }
+
+ rank-profile default {
+ first-phase {
+ expression: classicRank
+ keep-rank-count: 20
+ rank-score-drop-limit: -0.5
+ }
+ second-phase {
+ expression: if(3>2,4,2)
+ rerank-count: 10
+ }
+ rank-features: attribute(foo1).out attribute(bar1.out)
+ rank-features { attribute(foo2).out attribute(bar2).out }
+ rank-features {
+ attribute(foo3).out attribute(bar3).out }
+ rank-features {
+ attribute(foo4).out
+ attribute(bar4).out
+ }
+ ignore-default-rank-features
+
+ rank-properties {
+ foo: "bar, baz"
+ qux: "quux"
+ foo: "foobar"
+ foo.bar: "foo.bar"
+ foo.bar.baz: 123
+ foo ( bar ) . baz.2 : 123.4
+ foo(bar).baz.qux: "foo(bar)"
+ "nud":"ity"
+ }
+
+ }
+
+ rank-profile static {
+ first-phase {
+ expression { attribute }
+ }
+ second-phase {
+ expression {
+ file:rankexpression
+ }
+ }
+ summary-features: attribute(foo1).out attribute(bar1.out)
+ summary-features { attribute(foo2).out attribute(bar2).out }
+ summary-features {
+ attribute(foo3).out attribute(bar3).out }
+ summary-features {
+ attribute(foo4).out
+ attribute(bar4).out
+ }
+ }
+
+ rank-profile overflow {
+ first-phase {
+ expression: file:overflow.expression
+ keep-rank-count: 201
+ rank-score-drop-limit: 501.5
+ }
+ second-phase {
+ expression {
+ exp(0) +
+ mysum(attribute(foo),
+ "attribute( bar )",
+ "attribute( \"baz\" )")
+ }
+ rerank-count: 101
+ }
+ }
+
+ rank-profile duplicates {
+ rank-properties {
+ fieldMatch(a).proximityLimit: 4
+ fieldMatch(a).proximityTable: 0.2
+ fieldMatch(a).proximityTable: 0.4
+ fieldMatch(a).proximityTable: 0.6
+ fieldMatch(a).proximityTable: 0.8
+ fieldMatch(a).proximityTable: 1
+ fieldMatch(a).proximityTable: 0.8
+ fieldMatch(a).proximityTable: 0.6
+ fieldMatch(a).proximityTable: 0.4
+ fieldMatch(a).proximityTable: 0.2
+ }
+ }
+
+ rank-profile whitespace1 {
+ first-phase {
+ expression
+ {
+
+ 1
+ }}}
+
+ rank-profile whitespace2 {
+ first-phase
+ {
+ expression { 1 }
+ }
+ }
+
+ rank-profile macros {
+ first-phase {
+ expression: match + fieldMatch(title) + myfeature
+ }
+ second-phase {
+ expression: fourtimessum(match,rankBoost)
+ }
+ macro fourtimessum(var1, var2) {
+ expression: 4*(var1+var2)
+ }
+ macro myfeature() {
+ expression {
+ 70 * fieldMatch(title).completeness * pow(0 - fieldMatch(title).earliness, 2) +
+ 30 * pow(0 - fieldMatch(description).earliness, 2)
+ }
+ }
+ summary-features {
+ fieldMatch(title)
+ }
+ }
+
+ rank-profile macros2 {
+ first-phase {
+ expression: classicRank
+ }
+ rank-properties {
+ foo: "some, list"
+ }
+
+ second-phase {
+ expression: fourtimessum(match,match) + mysummaryfeature + myfeature
+ }
+ macro fourtimessum(var1, var2) {
+ expression: 4*(var1+var2)
+ }
+ macro myfeature() {
+ expression {
+ 70 * fieldMatch(title).completeness * pow(0 - fieldMatch(title).earliness, 2) +
+ 30 * pow(0 - fieldMatch(description).earliness, 2)
+ }
+ }
+ macro mysummaryfeature() {
+ expression {
+ 70 * fieldMatch(title).completeness
+ }
+ }
+ macro mysummaryfeature2() {
+ expression {
+ 71 * fieldMatch(title).completeness
+ }
+ }
+ summary-features {
+ mysummaryfeature
+ rankingExpression(mysummaryfeature2) # Required form earlier
+ }
+ }
+
+ rank-profile macros3 {
+ macro onlyusedinsummaryfeature() {
+ expression: 5
+ }
+ summary-features {
+ rankingExpression(matches(title,rankingExpression(onlyusedinsummaryfeature)))
+ }
+
+ }
+
+ rank-profile macros3-inherited inherits macros3 {
+ summary-features {
+ rankingExpression(matches(title,rankingExpression(onlyusedinsummaryfeature)))
+ }
+ }
+
+ rank-profile macros-inherited inherits macros2 {
+ macro mysummaryfeature() {
+ expression {
+ 80 * fieldMatch(title).completeness
+ }
+ }
+ first-phase {
+ expression {
+ 20000 * myfeature + mysummaryfeature
+ }
+ }
+ }
+
+ rank-profile macros-inherited2 inherits macros-inherited {
+ first-phase {
+ expression {
+ 30000 * mysummaryfeature + myfeature
+ }
+ }
+ }
+
+ rank-profile macros-inherited3 inherits macros-inherited2 {
+ macro myfeature() {
+ expression {
+ 700 * fieldMatch(title).completeness
+ }
+ }
+ second-phase {
+ expression {
+ 40000 * mysummaryfeature + myfeature
+ }
+ }
+ }
+
+ rank-profile macros-refering-macros {
+ macro m2() {
+ expression: m1 * 67
+ }
+
+ macro m1() {
+ expression {
+ 700 * fieldMatch(title).completeness
+ }
+ }
+
+ macro m4() {
+ expression: file:macro.expression
+ }
+
+ second-phase {
+ expression {
+ 40000 * m2
+ }
+ }
+
+ }
+
+ rank-profile macros-refering-macros-inherited inherits macros-refering-macros {
+ macro m3() {
+ expression {
+ if(isNan(attribute(nrtgmp))==1,
+ 0.0,
+ (m2)
+ )
+ }
+ }
+ macro m4() {
+ expression {
+ 701 * fieldMatch(title).completeness
+ }
+ }
+ second-phase {
+ expression {
+ 3000 * m2
+ }
+ }
+ }
+
+ rank-profile macros-refering-macros-inherited2 inherits macros-refering-macros {
+ second-phase {
+ expression {
+ 3002 * m2
+ }
+ }
+ }
+
+ rank-profile macros-refering-macros-inherited-two-levels inherits macros-refering-macros-inherited {
+ macro m5() {
+ expression {
+ if(isNan(attribute(glmpfw))==1,
+ m1,
+ (m4)
+ )
+ }
+ }
+ }
+
+}
+
+
diff --git a/config-model/src/test/derived/rankexpression/summary.cfg b/config-model/src/test/derived/rankexpression/summary.cfg
new file mode 100644
index 00000000000..00df2e87144
--- /dev/null
+++ b/config-model/src/test/derived/rankexpression/summary.cfg
@@ -0,0 +1,25 @@
+defaultsummaryid 1753207254
+classes[0].id 1753207254
+classes[0].name "default"
+classes[0].fields[0].name "artist"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "title"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "surl"
+classes[0].fields[2].type "longstring"
+classes[0].fields[3].name "year"
+classes[0].fields[3].type "integer"
+classes[0].fields[4].name "rankfeatures"
+classes[0].fields[4].type "featuredata"
+classes[0].fields[5].name "summaryfeatures"
+classes[0].fields[5].type "featuredata"
+classes[0].fields[6].name "documentid"
+classes[0].fields[6].type "longstring"
+classes[1].id 1787488393
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "year"
+classes[1].fields[0].type "integer"
+classes[1].fields[1].name "rankfeatures"
+classes[1].fields[1].type "featuredata"
+classes[1].fields[2].name "summaryfeatures"
+classes[1].fields[2].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/rankexpression/summarymap.cfg b/config-model/src/test/derived/rankexpression/summarymap.cfg
new file mode 100644
index 00000000000..c810f7282ba
--- /dev/null
+++ b/config-model/src/test/derived/rankexpression/summarymap.cfg
@@ -0,0 +1,10 @@
+defaultoutputclass -1
+override[0].field "year"
+override[0].command "attribute"
+override[0].arguments "year"
+override[1].field "rankfeatures"
+override[1].command "rankfeatures"
+override[1].arguments ""
+override[2].field "summaryfeatures"
+override[2].command "summaryfeatures"
+override[2].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/rankprofiles/rank-profiles.cfg b/config-model/src/test/derived/rankprofiles/rank-profiles.cfg
new file mode 100644
index 00000000000..da619d62a15
--- /dev/null
+++ b/config-model/src/test/derived/rankprofiles/rank-profiles.cfg
@@ -0,0 +1,98 @@
+rankprofile[0].name "default"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[2].name "other1"
+rankprofile[2].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.field1"
+rankprofile[2].fef.property[0].value "linear(0,0)"
+rankprofile[2].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.field1"
+rankprofile[2].fef.property[1].value "linear(0,0)"
+rankprofile[2].fef.property[2].name "nativeProximity.proximityTable.field1"
+rankprofile[2].fef.property[2].value "linear(0,0)"
+rankprofile[2].fef.property[3].name "nativeProximity.reverseProximityTable.field1"
+rankprofile[2].fef.property[3].value "linear(0,0)"
+rankprofile[2].fef.property[4].name "vespa.isfilterfield.field1"
+rankprofile[2].fef.property[4].value "true"
+rankprofile[3].name "other2"
+rankprofile[3].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.field1"
+rankprofile[3].fef.property[0].value "linear(0,0)"
+rankprofile[3].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.field1"
+rankprofile[3].fef.property[1].value "linear(0,0)"
+rankprofile[3].fef.property[2].name "nativeProximity.proximityTable.field1"
+rankprofile[3].fef.property[2].value "linear(0,0)"
+rankprofile[3].fef.property[3].name "nativeProximity.reverseProximityTable.field1"
+rankprofile[3].fef.property[3].value "linear(0,0)"
+rankprofile[3].fef.property[4].name "vespa.isfilterfield.field1"
+rankprofile[3].fef.property[4].value "true"
+rankprofile[4].name "other3"
+rankprofile[5].name "four"
+rankprofile[5].fef.property[0].name "vespa.matchphase.degradation.attribute"
+rankprofile[5].fef.property[0].value "field2"
+rankprofile[5].fef.property[1].name "vespa.matchphase.degradation.ascendingorder"
+rankprofile[5].fef.property[1].value "true"
+rankprofile[5].fef.property[2].name "vespa.matchphase.degradation.maxhits"
+rankprofile[5].fef.property[2].value "12345"
+rankprofile[5].fef.property[3].name "vespa.matchphase.degradation.maxfiltercoverage"
+rankprofile[5].fef.property[3].value "1.0"
+rankprofile[5].fef.property[4].name "vespa.matchphase.degradation.samplepercentage"
+rankprofile[5].fef.property[4].value "0.2"
+rankprofile[5].fef.property[5].name "vespa.matchphase.degradation.postfiltermultiplier"
+rankprofile[5].fef.property[5].value "1.0"
+rankprofile[6].name "five"
+rankprofile[6].fef.property[0].name "vespa.matchphase.degradation.attribute"
+rankprofile[6].fef.property[0].value "field2"
+rankprofile[6].fef.property[1].name "vespa.matchphase.degradation.ascendingorder"
+rankprofile[6].fef.property[1].value "false"
+rankprofile[6].fef.property[2].name "vespa.matchphase.degradation.maxhits"
+rankprofile[6].fef.property[2].value "54321"
+rankprofile[6].fef.property[3].name "vespa.matchphase.degradation.maxfiltercoverage"
+rankprofile[6].fef.property[3].value "1.0"
+rankprofile[6].fef.property[4].name "vespa.matchphase.degradation.samplepercentage"
+rankprofile[6].fef.property[4].value "0.2"
+rankprofile[6].fef.property[5].name "vespa.matchphase.degradation.postfiltermultiplier"
+rankprofile[6].fef.property[5].value "1.0"
+rankprofile[7].name "six"
+rankprofile[7].fef.property[0].name "vespa.matchphase.degradation.attribute"
+rankprofile[7].fef.property[0].value "field3"
+rankprofile[7].fef.property[1].name "vespa.matchphase.degradation.ascendingorder"
+rankprofile[7].fef.property[1].value "false"
+rankprofile[7].fef.property[2].name "vespa.matchphase.degradation.maxhits"
+rankprofile[7].fef.property[2].value "666"
+rankprofile[7].fef.property[3].name "vespa.matchphase.degradation.maxfiltercoverage"
+rankprofile[7].fef.property[3].value "1.0"
+rankprofile[7].fef.property[4].name "vespa.matchphase.degradation.samplepercentage"
+rankprofile[7].fef.property[4].value "0.2"
+rankprofile[7].fef.property[5].name "vespa.matchphase.degradation.postfiltermultiplier"
+rankprofile[7].fef.property[5].value "1.0"
+rankprofile[8].name "seven"
+rankprofile[8].fef.property[0].name "vespa.matchphase.degradation.attribute"
+rankprofile[8].fef.property[0].value "field3"
+rankprofile[8].fef.property[1].name "vespa.matchphase.degradation.ascendingorder"
+rankprofile[8].fef.property[1].value "false"
+rankprofile[8].fef.property[2].name "vespa.matchphase.degradation.maxhits"
+rankprofile[8].fef.property[2].value "800"
+rankprofile[8].fef.property[3].name "vespa.matchphase.degradation.maxfiltercoverage"
+rankprofile[8].fef.property[3].value "1.0"
+rankprofile[8].fef.property[4].name "vespa.matchphase.degradation.samplepercentage"
+rankprofile[8].fef.property[4].value "0.7"
+rankprofile[8].fef.property[5].name "vespa.matchphase.degradation.postfiltermultiplier"
+rankprofile[8].fef.property[5].value "3.4"
+rankprofile[9].name "eight"
+rankprofile[9].fef.property[0].name "vespa.matchphase.degradation.attribute"
+rankprofile[9].fef.property[0].value "field3"
+rankprofile[9].fef.property[1].name "vespa.matchphase.degradation.ascendingorder"
+rankprofile[9].fef.property[1].value "false"
+rankprofile[9].fef.property[2].name "vespa.matchphase.degradation.maxhits"
+rankprofile[9].fef.property[2].value "800"
+rankprofile[9].fef.property[3].name "vespa.matchphase.degradation.maxfiltercoverage"
+rankprofile[9].fef.property[3].value "1.0"
+rankprofile[9].fef.property[4].name "vespa.matchphase.degradation.samplepercentage"
+rankprofile[9].fef.property[4].value "0.7"
+rankprofile[9].fef.property[5].name "vespa.matchphase.degradation.postfiltermultiplier"
+rankprofile[9].fef.property[5].value "3.4" \ No newline at end of file
diff --git a/config-model/src/test/derived/rankprofiles/rankprofiles.sd b/config-model/src/test/derived/rankprofiles/rankprofiles.sd
new file mode 100644
index 00000000000..ea3be070344
--- /dev/null
+++ b/config-model/src/test/derived/rankprofiles/rankprofiles.sd
@@ -0,0 +1,72 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search rankprofiles {
+
+ document rankprofiles {
+
+ field field1 type string {
+ indexing: index
+ }
+
+ field field2 type int {
+ indexing: attribute
+ attribute: fast-search
+ }
+
+ field field3 type int {
+ indexing: attribute
+ attribute: fast-search
+ }
+
+ }
+
+ rank-profile default {
+ }
+
+ rank-profile other1 inherits default {
+ rank field1: filter
+ rank none: filter
+ }
+
+ rank-profile other2 inherits other1 {
+ }
+
+ rank-profile other3 {
+ }
+
+ rank-profile four {
+ match-phase {
+ attribute: field2
+ order: ascending
+ max-hits: 12345
+ }
+ }
+
+ rank-profile five {
+ match-phase {
+ attribute: field2
+ order: descending
+ max-hits: 54321
+ }
+ }
+
+ rank-profile six {
+ match-phase {
+ attribute: field3
+ max-hits: 666
+ }
+ }
+
+ rank-profile seven {
+ match-phase {
+ attribute: field3
+ max-hits:800
+ evaluation-point:0.7
+ pre-post-filter-tipping-point:3.4
+ }
+ }
+
+ rank-profile eight inherits seven {
+
+ }
+
+}
diff --git a/config-model/src/test/derived/rankproperties/rank-profiles.cfg b/config-model/src/test/derived/rankproperties/rank-profiles.cfg
new file mode 100644
index 00000000000..147153c2f62
--- /dev/null
+++ b/config-model/src/test/derived/rankproperties/rank-profiles.cfg
@@ -0,0 +1,48 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "$test"
+rankprofile[0].fef.property[0].value "foo"
+rankprofile[0].fef.property[1].name "vespa.rank.firstphase"
+rankprofile[0].fef.property[1].value "nativeFieldMatch"
+rankprofile[0].fef.property[2].name "vespa.rank.secondphase"
+rankprofile[0].fef.property[2].value "match"
+rankprofile[0].fef.property[3].name "vespa.fieldweight.tag"
+rankprofile[0].fef.property[3].value "33"
+rankprofile[0].fef.property[4].name "vespa.fieldweight.title"
+rankprofile[0].fef.property[4].value "50"
+rankprofile[0].fef.property[5].name "vespa.fieldweight.description"
+rankprofile[0].fef.property[5].value "10"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[2].name "child"
+rankprofile[2].fef.property[0].name "$test"
+rankprofile[2].fef.property[0].value "foo"
+rankprofile[2].fef.property[1].name "vespa.rank.firstphase"
+rankprofile[2].fef.property[1].value "nativeFieldMatch"
+rankprofile[2].fef.property[2].name "vespa.rank.secondphase"
+rankprofile[2].fef.property[2].value "match"
+rankprofile[2].fef.property[3].name "vespa.fieldweight.title"
+rankprofile[2].fef.property[3].value "15"
+rankprofile[2].fef.property[4].name "vespa.fieldweight.tag"
+rankprofile[2].fef.property[4].value "33"
+rankprofile[2].fef.property[5].name "vespa.fieldweight.description"
+rankprofile[2].fef.property[5].value "10"
+rankprofile[3].name "standalone"
+rankprofile[3].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[3].fef.property[0].value "rankingExpression(firstphase)"
+rankprofile[3].fef.property[1].name "rankingExpression(firstphase).rankingScript"
+rankprofile[3].fef.property[1].value "match + fieldMatch(title)"
+rankprofile[3].fef.property[2].name "vespa.rank.secondphase"
+rankprofile[3].fef.property[2].value "rankingExpression(secondphase)"
+rankprofile[3].fef.property[3].name "rankingExpression(secondphase).rankingScript"
+rankprofile[3].fef.property[3].value "match + nativeFieldMatch"
+rankprofile[3].fef.property[4].name "vespa.fieldweight.description"
+rankprofile[3].fef.property[4].value "35"
+rankprofile[3].fef.property[5].name "vespa.fieldweight.tag"
+rankprofile[3].fef.property[5].value "88" \ No newline at end of file
diff --git a/config-model/src/test/derived/rankproperties/rankproperties.sd b/config-model/src/test/derived/rankproperties/rankproperties.sd
new file mode 100644
index 00000000000..80d5a0cef44
--- /dev/null
+++ b/config-model/src/test/derived/rankproperties/rankproperties.sd
@@ -0,0 +1,61 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search rankproperties {
+
+ document rankproperties {
+
+ field title type string {
+ indexing: index
+ weight: 50
+ }
+
+ field description type string {
+ indexing: index
+ weight: 10
+ }
+
+ field tag type string {
+ indexing: attribute
+ }
+
+ }
+
+ rank-profile default {
+ weight tag: 33
+ first-phase {
+ expression: nativeFieldMatch
+ }
+ second-phase {
+ expression: match
+ }
+ rank-properties {
+ $test:"foo"
+ #$weight:1
+ }
+ }
+
+ rank-profile child inherits default {
+ weight title: 15
+ #rank-properties {
+ # $test:"bar"
+ # $weight:2
+ #}
+ #first-phase {
+ # expression {
+ # $weight
+ # }
+ #}
+ }
+
+ rank-profile standalone {
+ weight description: 35
+ weight tag: 88
+
+ first-phase {
+ expression: match + fieldMatch(title)
+ }
+ second-phase {
+ expression: match + nativeFieldMatch
+ }
+ }
+
+}
diff --git a/config-model/src/test/derived/ranktypes/attributes.cfg b/config-model/src/test/derived/ranktypes/attributes.cfg
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/attributes.cfg
diff --git a/config-model/src/test/derived/ranktypes/documentmanager.cfg b/config-model/src/test/derived/ranktypes/documentmanager.cfg
new file mode 100644
index 00000000000..c57a58c244c
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/documentmanager.cfg
@@ -0,0 +1,51 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -471393776
+datatype[1].structtype[0].name "ranktypes.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "title"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[1].structtype[0].field[1].name "descr"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "keywords"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[1].structtype[0].field[3].name "identity"
+datatype[1].structtype[0].field[3].datatype 2
+datatype[1].structtype[0].field[4].name "identity_literal"
+datatype[1].structtype[0].field[4].datatype 2
+datatype[1].structtype[0].field[5].name "rankfeatures"
+datatype[1].structtype[0].field[5].datatype 2
+datatype[1].structtype[0].field[6].name "summaryfeatures"
+datatype[1].structtype[0].field[6].datatype 2
+datatype[2].id 1374506021
+datatype[2].structtype[0].name "ranktypes.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[3].id -883421617
+datatype[3].documenttype[0].name "ranktypes"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct -471393776
+datatype[3].documenttype[0].bodystruct 1374506021
+datatype[3].documenttype[0].fieldsets{[document]}.fields[0] "descr"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[1] "identity"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[2] "keywords"
+datatype[3].documenttype[0].fieldsets{[document]}.fields[3] "title"
diff --git a/config-model/src/test/derived/ranktypes/ilscripts.cfg b/config-model/src/test/derived/ranktypes/ilscripts.cfg
new file mode 100644
index 00000000000..3a917099bfc
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/ilscripts.cfg
@@ -0,0 +1,11 @@
+maxtermoccurrences 100
+ilscript[0].doctype "ranktypes"
+ilscript[0].docfield[0] "title"
+ilscript[0].docfield[1] "descr"
+ilscript[0].docfield[2] "keywords"
+ilscript[0].docfield[3] "identity"
+ilscript[0].content[0] "clear_state | guard { input identity | tokenize | index identity_literal; }"
+ilscript[0].content[1] "clear_state | guard { input title | tokenize normalize stem:\"SHORTEST\" | summary title | index title; }"
+ilscript[0].content[2] "clear_state | guard { input descr | tokenize normalize stem:\"SHORTEST\" | summary descr | index descr; }"
+ilscript[0].content[3] "clear_state | guard { input keywords | tokenize normalize stem:\"SHORTEST\" | index keywords; }"
+ilscript[0].content[4] "clear_state | guard { input identity | tokenize normalize stem:\"SHORTEST\" | index identity; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/ranktypes/index-info.cfg b/config-model/src/test/derived/ranktypes/index-info.cfg
new file mode 100644
index 00000000000..464f7a73676
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/index-info.cfg
@@ -0,0 +1,47 @@
+indexinfo[0].name "ranktypes"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "title"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "title"
+indexinfo[0].command[3].command "lowercase"
+indexinfo[0].command[4].indexname "title"
+indexinfo[0].command[4].command "stem:SHORTEST"
+indexinfo[0].command[5].indexname "title"
+indexinfo[0].command[5].command "normalize"
+indexinfo[0].command[6].indexname "descr"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "descr"
+indexinfo[0].command[7].command "lowercase"
+indexinfo[0].command[8].indexname "descr"
+indexinfo[0].command[8].command "stem:SHORTEST"
+indexinfo[0].command[9].indexname "descr"
+indexinfo[0].command[9].command "normalize"
+indexinfo[0].command[10].indexname "keywords"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "keywords"
+indexinfo[0].command[11].command "lowercase"
+indexinfo[0].command[12].indexname "keywords"
+indexinfo[0].command[12].command "stem:SHORTEST"
+indexinfo[0].command[13].indexname "keywords"
+indexinfo[0].command[13].command "normalize"
+indexinfo[0].command[14].indexname "identity"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "identity"
+indexinfo[0].command[15].command "lowercase"
+indexinfo[0].command[16].indexname "identity"
+indexinfo[0].command[16].command "stem:SHORTEST"
+indexinfo[0].command[17].indexname "identity"
+indexinfo[0].command[17].command "normalize"
+indexinfo[0].command[18].indexname "identity"
+indexinfo[0].command[18].command "literal-boost"
+indexinfo[0].command[19].indexname "identity_literal"
+indexinfo[0].command[19].command "index"
+indexinfo[0].command[20].indexname "identity_literal"
+indexinfo[0].command[20].command "lowercase"
+indexinfo[0].command[21].indexname "rankfeatures"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "summaryfeatures"
+indexinfo[0].command[22].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/ranktypes/rank-profiles.cfg b/config-model/src/test/derived/ranktypes/rank-profiles.cfg
new file mode 100644
index 00000000000..aee11c0bbef
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/rank-profiles.cfg
@@ -0,0 +1,79 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.identity_literal"
+rankprofile[0].fef.property[0].value "linear(0,0)"
+rankprofile[0].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.identity_literal"
+rankprofile[0].fef.property[1].value "linear(0,0)"
+rankprofile[0].fef.property[2].name "nativeProximity.proximityTable.identity_literal"
+rankprofile[0].fef.property[2].value "linear(0,0)"
+rankprofile[0].fef.property[3].name "nativeProximity.reverseProximityTable.identity_literal"
+rankprofile[0].fef.property[3].value "linear(0,0)"
+rankprofile[0].fef.property[4].name "nativeFieldMatch.firstOccurrenceTable.title"
+rankprofile[0].fef.property[4].value "expdecay(100,12.50)"
+rankprofile[0].fef.property[5].name "nativeFieldMatch.occurrenceCountTable.title"
+rankprofile[0].fef.property[5].value "loggrowth(1500,4000,19)"
+rankprofile[0].fef.property[6].name "nativeProximity.proximityTable.title"
+rankprofile[0].fef.property[6].value "expdecay(5000,3)"
+rankprofile[0].fef.property[7].name "nativeProximity.reverseProximityTable.title"
+rankprofile[0].fef.property[7].value "expdecay(3000,3)"
+rankprofile[0].fef.property[8].name "nativeFieldMatch.firstOccurrenceTable.descr"
+rankprofile[0].fef.property[8].value "expdecay(8000,12.50)"
+rankprofile[0].fef.property[9].name "nativeFieldMatch.occurrenceCountTable.descr"
+rankprofile[0].fef.property[9].value "loggrowth(1500,4000,19)"
+rankprofile[0].fef.property[10].name "nativeProximity.proximityTable.descr"
+rankprofile[0].fef.property[10].value "expdecay(500,3)"
+rankprofile[0].fef.property[11].name "nativeProximity.reverseProximityTable.descr"
+rankprofile[0].fef.property[11].value "expdecay(400,3)"
+rankprofile[0].fef.property[12].name "nativeFieldMatch.firstOccurrenceTable.keywords"
+rankprofile[0].fef.property[12].value "expdecay(8000,12.50)"
+rankprofile[0].fef.property[13].name "nativeFieldMatch.occurrenceCountTable.keywords"
+rankprofile[0].fef.property[13].value "loggrowth(1500,4000,19)"
+rankprofile[0].fef.property[14].name "nativeProximity.proximityTable.keywords"
+rankprofile[0].fef.property[14].value "expdecay(500,3)"
+rankprofile[0].fef.property[15].name "nativeProximity.reverseProximityTable.keywords"
+rankprofile[0].fef.property[15].value "expdecay(400,3)"
+rankprofile[0].fef.property[16].name "vespa.fieldweight.identity_literal"
+rankprofile[0].fef.property[16].value "200"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true"
+rankprofile[2].name "override"
+rankprofile[2].fef.property[0].name "nativeFieldMatch.firstOccurrenceTable.descr"
+rankprofile[2].fef.property[0].value "expdecay(100,12.50)"
+rankprofile[2].fef.property[1].name "nativeFieldMatch.occurrenceCountTable.descr"
+rankprofile[2].fef.property[1].value "loggrowth(1500,4000,19)"
+rankprofile[2].fef.property[2].name "nativeProximity.proximityTable.descr"
+rankprofile[2].fef.property[2].value "expdecay(5000,3)"
+rankprofile[2].fef.property[3].name "nativeProximity.reverseProximityTable.descr"
+rankprofile[2].fef.property[3].value "expdecay(3000,3)"
+rankprofile[2].fef.property[4].name "nativeFieldMatch.firstOccurrenceTable.identity_literal"
+rankprofile[2].fef.property[4].value "linear(0,0)"
+rankprofile[2].fef.property[5].name "nativeFieldMatch.occurrenceCountTable.identity_literal"
+rankprofile[2].fef.property[5].value "linear(0,0)"
+rankprofile[2].fef.property[6].name "nativeProximity.proximityTable.identity_literal"
+rankprofile[2].fef.property[6].value "linear(0,0)"
+rankprofile[2].fef.property[7].name "nativeProximity.reverseProximityTable.identity_literal"
+rankprofile[2].fef.property[7].value "linear(0,0)"
+rankprofile[2].fef.property[8].name "nativeFieldMatch.firstOccurrenceTable.title"
+rankprofile[2].fef.property[8].value "expdecay(100,12.50)"
+rankprofile[2].fef.property[9].name "nativeFieldMatch.occurrenceCountTable.title"
+rankprofile[2].fef.property[9].value "loggrowth(1500,4000,19)"
+rankprofile[2].fef.property[10].name "nativeProximity.proximityTable.title"
+rankprofile[2].fef.property[10].value "expdecay(5000,3)"
+rankprofile[2].fef.property[11].name "nativeProximity.reverseProximityTable.title"
+rankprofile[2].fef.property[11].value "expdecay(3000,3)"
+rankprofile[2].fef.property[12].name "nativeFieldMatch.firstOccurrenceTable.keywords"
+rankprofile[2].fef.property[12].value "expdecay(8000,12.50)"
+rankprofile[2].fef.property[13].name "nativeFieldMatch.occurrenceCountTable.keywords"
+rankprofile[2].fef.property[13].value "loggrowth(1500,4000,19)"
+rankprofile[2].fef.property[14].name "nativeProximity.proximityTable.keywords"
+rankprofile[2].fef.property[14].value "expdecay(500,3)"
+rankprofile[2].fef.property[15].name "nativeProximity.reverseProximityTable.keywords"
+rankprofile[2].fef.property[15].value "expdecay(400,3)"
+rankprofile[2].fef.property[16].name "vespa.fieldweight.identity_literal"
+rankprofile[2].fef.property[16].value "200" \ No newline at end of file
diff --git a/config-model/src/test/derived/ranktypes/ranktypes.sd b/config-model/src/test/derived/ranktypes/ranktypes.sd
new file mode 100644
index 00000000000..e9fc8606e4c
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/ranktypes.sd
@@ -0,0 +1,32 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search ranktypes {
+
+ document ranktypes {
+
+ field title type string {
+ indexing: summary | index
+ rank-type: identity
+ }
+
+ field descr type string {
+ indexing: summary | index
+ rank-type: about
+ }
+
+ field keywords type string {
+ indexing: index
+ rank-type: tags
+ }
+
+ field identity type string {
+ indexing: index
+ rank: literal
+ }
+
+ }
+
+ rank-profile override inherits default {
+ rank-type descr: identity
+ }
+
+}
diff --git a/config-model/src/test/derived/ranktypes/summary.cfg b/config-model/src/test/derived/ranktypes/summary.cfg
new file mode 100644
index 00000000000..aaf8398b303
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/summary.cfg
@@ -0,0 +1,13 @@
+defaultsummaryid 1567556360
+classes[0].id 1567556360
+classes[0].name "default"
+classes[0].fields[0].name "title"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "descr"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "rankfeatures"
+classes[0].fields[2].type "featuredata"
+classes[0].fields[3].name "summaryfeatures"
+classes[0].fields[3].type "featuredata"
+classes[0].fields[4].name "documentid"
+classes[0].fields[4].type "longstring" \ No newline at end of file
diff --git a/config-model/src/test/derived/ranktypes/summarymap.cfg b/config-model/src/test/derived/ranktypes/summarymap.cfg
new file mode 100644
index 00000000000..42b6e811ee6
--- /dev/null
+++ b/config-model/src/test/derived/ranktypes/summarymap.cfg
@@ -0,0 +1,7 @@
+defaultoutputclass -1
+override[0].field "rankfeatures"
+override[0].command "rankfeatures"
+override[0].arguments ""
+override[1].field "summaryfeatures"
+override[1].command "summaryfeatures"
+override[1].arguments "" \ No newline at end of file
diff --git a/config-model/src/test/derived/reserved_position/reserved_position.sd b/config-model/src/test/derived/reserved_position/reserved_position.sd
new file mode 100644
index 00000000000..bee62a646bf
--- /dev/null
+++ b/config-model/src/test/derived/reserved_position/reserved_position.sd
@@ -0,0 +1,4 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position {
+ document position { }
+}
diff --git a/config-model/src/test/derived/sorting/attributes.cfg b/config-model/src/test/derived/sorting/attributes.cfg
new file mode 100644
index 00000000000..c65271c618a
--- /dev/null
+++ b/config-model/src/test/derived/sorting/attributes.cfg
@@ -0,0 +1,57 @@
+attribute[0].name "syntaxcheck"
+attribute[0].datatype STRING
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending false
+attribute[0].sortfunction LOWERCASE
+attribute[0].sortstrength IDENTICAL
+attribute[0].sortlocale "en_US"
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "syntaxcheck2"
+attribute[1].datatype STRING
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending false
+attribute[1].sortfunction LOWERCASE
+attribute[1].sortstrength IDENTICAL
+attribute[1].sortlocale "en_US"
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "infieldonly"
+attribute[2].datatype STRING
+attribute[2].collectiontype SINGLE
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending false
+attribute[2].sortfunction LOWERCASE
+attribute[2].sortstrength SECONDARY
+attribute[2].sortlocale "en_US"
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype "" \ No newline at end of file
diff --git a/config-model/src/test/derived/sorting/sorting.sd b/config-model/src/test/derived/sorting/sorting.sd
new file mode 100644
index 00000000000..c67222a56ed
--- /dev/null
+++ b/config-model/src/test/derived/sorting/sorting.sd
@@ -0,0 +1,53 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search sorting {
+
+ document sorting {
+
+ field syntaxcheck type string {
+ indexing: attribute
+ attribute {
+ sorting: ascending
+ sorting: descending
+ sorting: function: uca
+ sorting: function: raw
+ sorting: function: lowercase
+ sorting: strength: primary
+ sorting: strength: secondary
+ sorting: strength: tertiary
+ sorting: strength: quaternary
+ sorting: strength: identical
+ sorting: locale: en_US
+ }
+ }
+
+ field syntaxcheck2 type string {
+ indexing: attribute
+ attribute {
+ sorting {
+ ascending
+ descending
+ function: uca
+ function: raw
+ function: lowercase
+ strength: primary
+ strength: secondary
+ strength: tertiary
+ strength: quaternary
+ strength: identical
+ locale: en_US
+ }
+ }
+ }
+
+ field infieldonly type string {
+ indexing: attribute
+ sorting {
+ descending
+ function: lowercase
+ strength: secondary
+ locale: en_US
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/test/derived/streamingjuniper/streamingjuniper.sd b/config-model/src/test/derived/streamingjuniper/streamingjuniper.sd
new file mode 100644
index 00000000000..b7fb367112a
--- /dev/null
+++ b/config-model/src/test/derived/streamingjuniper/streamingjuniper.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search streamingjuniper {
+ document streamingjuniper {
+ field f1 type string {
+ indexing: index | summary
+ header
+ bolding: on
+ }
+ field f2 type string {
+ indexing: index | summary
+ header
+ summary: dynamic
+ }
+ }
+}
diff --git a/config-model/src/test/derived/streamingjuniper/vsmsummary.cfg b/config-model/src/test/derived/streamingjuniper/vsmsummary.cfg
new file mode 100644
index 00000000000..939f3453e3b
--- /dev/null
+++ b/config-model/src/test/derived/streamingjuniper/vsmsummary.cfg
@@ -0,0 +1,11 @@
+outputclass ""
+fieldmap[0].summary "f1"
+fieldmap[0].document[0].field "f1"
+fieldmap[0].command FLATTENJUNIPER
+fieldmap[1].summary "f2"
+fieldmap[1].document[0].field "f2"
+fieldmap[1].command FLATTENJUNIPER
+fieldmap[2].summary "rankfeatures"
+fieldmap[2].command NONE
+fieldmap[3].summary "summaryfeatures"
+fieldmap[3].command NONE \ No newline at end of file
diff --git a/config-model/src/test/derived/streamingstruct/documentmanager.cfg b/config-model/src/test/derived/streamingstruct/documentmanager.cfg
new file mode 100644
index 00000000000..306f1102fb1
--- /dev/null
+++ b/config-model/src/test/derived/streamingstruct/documentmanager.cfg
@@ -0,0 +1,127 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 105061838
+datatype[1].structtype[0].name "ns1"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "nf1"
+datatype[1].structtype[0].field[0].datatype 3474528
+datatype[1].structtype[0].field[1].name "nf1s"
+datatype[1].structtype[0].field[1].datatype 3474528
+datatype[1].structtype[0].field[2].name "nf2"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[2].id 3474528
+datatype[2].structtype[0].name "s1"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "f1"
+datatype[2].structtype[0].field[0].datatype 2
+datatype[2].structtype[0].field[1].name "f1s"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[2].structtype[0].field[2].name "f2"
+datatype[2].structtype[0].field[2].datatype 0
+datatype[2].structtype[0].field[3].name "f3"
+datatype[2].structtype[0].field[3].datatype 5
+datatype[3].id -1497802371
+datatype[3].maptype[0].keytype 4
+datatype[3].maptype[0].valtype 2
+datatype[4].id -1425630723
+datatype[4].arraytype[0].datatype 3474528
+datatype[5].id 731395686
+datatype[5].structtype[0].name "streamingstruct.header"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "coupleof"
+datatype[5].structtype[0].field[0].datatype 2
+datatype[5].structtype[0].field[1].name "normalfields"
+datatype[5].structtype[0].field[1].datatype 2
+datatype[5].structtype[0].field[2].name "a"
+datatype[5].structtype[0].field[2].datatype 3474528
+datatype[5].structtype[0].field[3].name "m"
+datatype[5].structtype[0].field[3].datatype -1497802371
+datatype[5].structtype[0].field[4].name "b"
+datatype[5].structtype[0].field[4].datatype 3474528
+datatype[5].structtype[0].field[5].name "c"
+datatype[5].structtype[0].field[5].datatype 3474528
+datatype[5].structtype[0].field[6].name "c2"
+datatype[5].structtype[0].field[6].datatype 3474528
+datatype[5].structtype[0].field[7].name "c3"
+datatype[5].structtype[0].field[7].datatype 3474528
+datatype[5].structtype[0].field[8].name "n"
+datatype[5].structtype[0].field[8].datatype 105061838
+datatype[5].structtype[0].field[9].name "array1"
+datatype[5].structtype[0].field[9].datatype -1425630723
+datatype[5].structtype[0].field[10].name "array2"
+datatype[5].structtype[0].field[10].datatype -1425630723
+datatype[5].structtype[0].field[11].name "array3"
+datatype[5].structtype[0].field[11].datatype -1425630723
+datatype[5].structtype[0].field[12].name "subject"
+datatype[5].structtype[0].field[12].datatype 3474528
+datatype[5].structtype[0].field[13].name "d"
+datatype[5].structtype[0].field[13].datatype 3474528
+datatype[5].structtype[0].field[14].name "e"
+datatype[5].structtype[0].field[14].datatype 3474528
+datatype[5].structtype[0].field[15].name "f"
+datatype[5].structtype[0].field[15].datatype 3474528
+datatype[5].structtype[0].field[16].name "g"
+datatype[5].structtype[0].field[16].datatype 2
+datatype[5].structtype[0].field[17].name "anothersummaryfield"
+datatype[5].structtype[0].field[17].datatype 2
+datatype[5].structtype[0].field[18].name "rankfeatures"
+datatype[5].structtype[0].field[18].datatype 2
+datatype[5].structtype[0].field[19].name "summaryfeatures"
+datatype[5].structtype[0].field[19].datatype 2
+datatype[5].structtype[0].field[20].name "snippet"
+datatype[5].structtype[0].field[20].datatype 2
+datatype[5].structtype[0].field[21].name "snippet2"
+datatype[5].structtype[0].field[21].datatype 2
+datatype[6].id 1858438651
+datatype[6].structtype[0].name "streamingstruct.body"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[7].id 1433175737
+datatype[7].documenttype[0].name "streamingstruct"
+datatype[7].documenttype[0].version 0
+datatype[7].documenttype[0].inherits[0].name "document"
+datatype[7].documenttype[0].inherits[0].version 0
+datatype[7].documenttype[0].headerstruct 731395686
+datatype[7].documenttype[0].bodystruct 1858438651
+datatype[7].documenttype[0].fieldsets{[document]}.fields[0] "a"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[1] "array1"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[2] "array2"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[3] "array3"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[4] "b"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[5] "c"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[6] "c2"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[7] "c3"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[8] "coupleof"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[9] "d"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[10] "e"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[11] "f"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[12] "g"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[13] "m"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[14] "n"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[15] "normalfields"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[16] "subject"
diff --git a/config-model/src/test/derived/streamingstruct/onlydoc/documentmanager.cfg b/config-model/src/test/derived/streamingstruct/onlydoc/documentmanager.cfg
new file mode 100644
index 00000000000..fc9b0e315b8
--- /dev/null
+++ b/config-model/src/test/derived/streamingstruct/onlydoc/documentmanager.cfg
@@ -0,0 +1,100 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 105061838
+datatype[1].structtype[0].name "ns1"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "nf1"
+datatype[1].structtype[0].field[0].datatype 3474528
+datatype[1].structtype[0].field[1].name "nf1s"
+datatype[1].structtype[0].field[1].datatype 3474528
+datatype[1].structtype[0].field[2].name "nf2"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[2].id 3474528
+datatype[2].structtype[0].name "s1"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "f1"
+datatype[2].structtype[0].field[0].datatype 2
+datatype[2].structtype[0].field[1].name "f1s"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[2].structtype[0].field[2].name "f2"
+datatype[2].structtype[0].field[2].datatype 0
+datatype[2].structtype[0].field[3].name "f3"
+datatype[2].structtype[0].field[3].datatype 5
+datatype[3].id -1497802371
+datatype[3].maptype[0].keytype 4
+datatype[3].maptype[0].valtype 2
+datatype[4].id -1425630723
+datatype[4].arraytype[0].datatype 3474528
+datatype[5].id 731395686
+datatype[5].structtype[0].name "streamingstruct.header"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "coupleof"
+datatype[5].structtype[0].field[0].datatype 2
+datatype[5].structtype[0].field[1].name "normalfields"
+datatype[5].structtype[0].field[1].datatype 2
+datatype[5].structtype[0].field[2].name "a"
+datatype[5].structtype[0].field[2].datatype 3474528
+datatype[5].structtype[0].field[3].name "m"
+datatype[5].structtype[0].field[3].datatype -1497802371
+datatype[5].structtype[0].field[4].name "b"
+datatype[5].structtype[0].field[4].datatype 3474528
+datatype[5].structtype[0].field[5].name "c"
+datatype[5].structtype[0].field[5].datatype 3474528
+datatype[5].structtype[0].field[6].name "c2"
+datatype[5].structtype[0].field[6].datatype 3474528
+datatype[5].structtype[0].field[7].name "c3"
+datatype[5].structtype[0].field[7].datatype 3474528
+datatype[5].structtype[0].field[8].name "n"
+datatype[5].structtype[0].field[8].datatype 105061838
+datatype[5].structtype[0].field[9].name "array1"
+datatype[5].structtype[0].field[9].datatype -1425630723
+datatype[5].structtype[0].field[10].name "array2"
+datatype[5].structtype[0].field[10].datatype -1425630723
+datatype[5].structtype[0].field[11].name "array3"
+datatype[5].structtype[0].field[11].datatype -1425630723
+datatype[5].structtype[0].field[12].name "subject"
+datatype[5].structtype[0].field[12].datatype 3474528
+datatype[5].structtype[0].field[13].name "d"
+datatype[5].structtype[0].field[13].datatype 3474528
+datatype[5].structtype[0].field[14].name "e"
+datatype[5].structtype[0].field[14].datatype 3474528
+datatype[5].structtype[0].field[15].name "f"
+datatype[5].structtype[0].field[15].datatype 3474528
+datatype[5].structtype[0].field[16].name "g"
+datatype[5].structtype[0].field[16].datatype 2
+datatype[6].id 1858438651
+datatype[6].structtype[0].name "streamingstruct.body"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[7].id 1433175737
+datatype[7].documenttype[0].name "streamingstruct"
+datatype[7].documenttype[0].version 0
+datatype[7].documenttype[0].inherits[0].name "document"
+datatype[7].documenttype[0].inherits[0].version 0
+datatype[7].documenttype[0].headerstruct 731395686
+datatype[7].documenttype[0].bodystruct 1858438651
diff --git a/config-model/src/test/derived/streamingstruct/streamingstruct.sd b/config-model/src/test/derived/streamingstruct/streamingstruct.sd
new file mode 100644
index 00000000000..c90afef3e46
--- /dev/null
+++ b/config-model/src/test/derived/streamingstruct/streamingstruct.sd
@@ -0,0 +1,184 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search streamingstruct {
+
+ document streamingstruct {
+ field coupleof type string {
+ indexing: index | summary
+ }
+
+ field normalfields type string {
+ indexing: index | summary
+ summary anothersummaryfield {
+ source: normalfields
+ }
+ }
+
+ struct s1 {
+ field f1 type string { }
+ field f1s type string { match: substring }
+ field f2 type int { }
+ field f3 type double { }
+ # Allow default matchtypes in struct. Can be overridden.
+ # No index/attribute related stuff. It is only a datatype definition.
+ }
+ struct ns1 {
+ field nf1 type s1 { }
+ field nf1s type s1 { match: substring }
+ field nf2 type string { }
+ # May specify nested structs.
+ }
+
+ field a type s1 {
+ indexing: summary
+ # Will generate attribute a.f1, a.f1s, a.f2, a.f3
+ # with datatypes preserved.
+ # No customisation.
+ # a will be a synonym for all (a.*)
+ }
+ field m type map<long, string> {
+ indexing: summary
+ # Will generate attribute m.key, m.value
+ # with datatypes preserved.
+ # m will be a synonym for all (m.*)
+ }
+ field b type s1 {
+ indexing: index | summary
+ # Will generate index b.f1, b.f1s, b.f2, b.f3
+ # with datatypes preserved if backend allows.
+ # No customisation.
+ # b will be synonym for all (b.*).
+ }
+ field c type s1 {
+ struct-field f1 {
+ # Whatever you normally write in a field declaration
+ indexing: attribute | summary # -> Generates attribute c.f1
+ }
+ struct-field f1s {
+ indexing: index | summary # -> c.f1s
+ match: prefix
+ }
+ struct-field f3 {
+ indexing: index | summary # -> c.f3
+ }
+ # attribute c will be synonym for c.f1.
+ # Index c will be synonym for c.f1s OR c.f3.
+ # Indexed search can handle that however they want.
+ }
+ field c2 type s1 {
+ struct-field f1 {
+ # Whatever you normally write in a field declaration
+ indexing: attribute | summary # -> Generates attribute c2.f1
+ }
+ struct-field f1s {
+ indexing: index | summary # -> c2.f1s
+ match: suffix
+ }
+ struct-field f2 {
+ indexing: index | summary # -> c2.f2
+ }
+ struct-field f3 {
+ indexing: index | summary # -> c2.f3
+ }
+ }
+ field c3 type s1 {
+ # Uses all sub fields, but not summary for all.
+ struct-field f1 {
+ indexing: attribute | summary
+ }
+ struct-field f1s {
+ indexing: index
+ match: prefix
+ }
+ struct-field f2 {
+ indexing: index | summary
+ }
+ struct-field f3 {
+ indexing: index | summary
+ }
+ }
+ field n type ns1 {
+ struct-field nf1 {
+ struct-field f1 {
+ indexing: index | summary
+ }
+ struct-field f1s {
+ indexing: index | summary
+ match: prefix
+ }
+ struct-field f3 {
+ indexing: index
+ }
+ }
+ struct-field nf1s {
+ indexing: index | summary
+ }
+ struct-field nf2 {
+ indexing: index | summary
+ }
+ # Will generate indexes n.nf1.f1, n.nf1.f1s, n.nf1.f3,
+ # n.nf1s.f1, n.nf1s.f1s, n.nf1s.f2, n.nf1s.f3
+ # and n.nf2.
+ # n will be synonym for all 8,
+ # n.nf1 will be synonym for the first 3 and
+ # n.nf1s will be synonym for the next 4
+ }
+
+ field array1 type array<s1> {
+ indexing: summary
+ }
+ field array2 type array<s1> {
+ indexing: index | summary
+ }
+ field array3 type array<s1> {
+ struct-field f1 {
+ indexing: attribute | summary # -> Generates attribute array3.f1
+ }
+ struct-field f1s {
+ indexing: index | summary # -> array3.f1s
+ match: prefix
+ }
+ struct-field f3 {
+ indexing: index | summary # -> array3.f3
+ }
+ }
+
+ field subject type s1 {
+ struct-field f1 {
+ indexing: summary
+ summary subject {
+ source:subject.f1
+ }
+ }
+ }
+ field d type s1 {
+ indexing: index
+ # override matching for all subfields
+ match: prefix
+ }
+ field e type s1 {
+ indexing: index
+ # override matching for all subfields
+ match: substring
+ }
+ field f type s1 {
+ indexing: index
+ # override matching for all subfields
+ match: suffix
+ }
+ field g type string {
+ indexing: index | summary
+ summary: dynamic
+ }
+ }
+
+ document-summary summ {
+ summary snippet type string {
+ dynamic
+ source: a.f1, b.f2
+ }
+ summary snippet2 type string {
+ source: a.f1, b.f1, b.f2
+ }
+ }
+
+}
diff --git a/config-model/src/test/derived/streamingstruct/summary.cfg b/config-model/src/test/derived/streamingstruct/summary.cfg
new file mode 100644
index 00000000000..9d722116850
--- /dev/null
+++ b/config-model/src/test/derived/streamingstruct/summary.cfg
@@ -0,0 +1,51 @@
+defaultsummaryid 569269436
+classes[0].id 569269436
+classes[0].name "default"
+classes[0].fields[0].name "coupleof"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "anothersummaryfield"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "a"
+classes[0].fields[2].type "jsonstring"
+classes[0].fields[3].name "m"
+classes[0].fields[3].type "jsonstring"
+classes[0].fields[4].name "b"
+classes[0].fields[4].type "jsonstring"
+classes[0].fields[5].name "c"
+classes[0].fields[5].type "jsonstring"
+classes[0].fields[6].name "c2"
+classes[0].fields[6].type "jsonstring"
+classes[0].fields[7].name "c3"
+classes[0].fields[7].type "jsonstring"
+classes[0].fields[8].name "n"
+classes[0].fields[8].type "jsonstring"
+classes[0].fields[9].name "array1"
+classes[0].fields[9].type "jsonstring"
+classes[0].fields[10].name "array2"
+classes[0].fields[10].type "jsonstring"
+classes[0].fields[11].name "array3"
+classes[0].fields[11].type "jsonstring"
+classes[0].fields[12].name "subject"
+classes[0].fields[12].type "jsonstring"
+classes[0].fields[13].name "g"
+classes[0].fields[13].type "longstring"
+classes[0].fields[14].name "rankfeatures"
+classes[0].fields[14].type "featuredata"
+classes[0].fields[15].name "summaryfeatures"
+classes[0].fields[15].type "featuredata"
+classes[0].fields[16].name "snippet"
+classes[0].fields[16].type "longstring"
+classes[0].fields[17].name "snippet2"
+classes[0].fields[17].type "longstring"
+classes[0].fields[18].name "documentid"
+classes[0].fields[18].type "longstring"
+classes[1].id 109252281
+classes[1].name "summ"
+classes[1].fields[0].name "snippet"
+classes[1].fields[0].type "longstring"
+classes[1].fields[1].name "snippet2"
+classes[1].fields[1].type "longstring"
+classes[1].fields[2].name "rankfeatures"
+classes[1].fields[2].type "featuredata"
+classes[1].fields[3].name "summaryfeatures"
+classes[1].fields[3].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/streamingstruct/summarymap.cfg b/config-model/src/test/derived/streamingstruct/summarymap.cfg
new file mode 100644
index 00000000000..1bf13ffa199
--- /dev/null
+++ b/config-model/src/test/derived/streamingstruct/summarymap.cfg
@@ -0,0 +1,13 @@
+defaultoutputclass -1
+override[0].field "snippet"
+override[0].command "dynamicteaser"
+override[0].arguments "snippet"
+override[1].field "rankfeatures"
+override[1].command "rankfeatures"
+override[1].arguments ""
+override[2].field "summaryfeatures"
+override[2].command "summaryfeatures"
+override[2].arguments ""
+override[3].field "g"
+override[3].command "dynamicteaser"
+override[3].arguments "g" \ No newline at end of file
diff --git a/config-model/src/test/derived/streamingstruct/vsmfields.cfg b/config-model/src/test/derived/streamingstruct/vsmfields.cfg
new file mode 100644
index 00000000000..abfd90820cc
--- /dev/null
+++ b/config-model/src/test/derived/streamingstruct/vsmfields.cfg
@@ -0,0 +1,434 @@
+documentverificationlevel 0
+searchall 1
+fieldspec[0].name "coupleof"
+fieldspec[0].searchmethod AUTOUTF8
+fieldspec[0].arg1 ""
+fieldspec[0].maxlength 1048576
+fieldspec[0].fieldtype INDEX
+fieldspec[1].name "normalfields"
+fieldspec[1].searchmethod AUTOUTF8
+fieldspec[1].arg1 ""
+fieldspec[1].maxlength 1048576
+fieldspec[1].fieldtype INDEX
+fieldspec[2].name "a.f1"
+fieldspec[2].searchmethod AUTOUTF8
+fieldspec[2].arg1 ""
+fieldspec[2].maxlength 1048576
+fieldspec[2].fieldtype INDEX
+fieldspec[3].name "a.f1s"
+fieldspec[3].searchmethod AUTOUTF8
+fieldspec[3].arg1 "substring"
+fieldspec[3].maxlength 1048576
+fieldspec[3].fieldtype INDEX
+fieldspec[4].name "a.f2"
+fieldspec[4].searchmethod INT32
+fieldspec[4].arg1 ""
+fieldspec[4].maxlength 1048576
+fieldspec[4].fieldtype INDEX
+fieldspec[5].name "a.f3"
+fieldspec[5].searchmethod DOUBLE
+fieldspec[5].arg1 ""
+fieldspec[5].maxlength 1048576
+fieldspec[5].fieldtype INDEX
+fieldspec[6].name "m.key"
+fieldspec[6].searchmethod INT64
+fieldspec[6].arg1 ""
+fieldspec[6].maxlength 1048576
+fieldspec[6].fieldtype INDEX
+fieldspec[7].name "m.value"
+fieldspec[7].searchmethod AUTOUTF8
+fieldspec[7].arg1 ""
+fieldspec[7].maxlength 1048576
+fieldspec[7].fieldtype INDEX
+fieldspec[8].name "b.f1"
+fieldspec[8].searchmethod AUTOUTF8
+fieldspec[8].arg1 ""
+fieldspec[8].maxlength 1048576
+fieldspec[8].fieldtype INDEX
+fieldspec[9].name "b.f1s"
+fieldspec[9].searchmethod AUTOUTF8
+fieldspec[9].arg1 "substring"
+fieldspec[9].maxlength 1048576
+fieldspec[9].fieldtype INDEX
+fieldspec[10].name "b.f2"
+fieldspec[10].searchmethod INT32
+fieldspec[10].arg1 ""
+fieldspec[10].maxlength 1048576
+fieldspec[10].fieldtype INDEX
+fieldspec[11].name "b.f3"
+fieldspec[11].searchmethod DOUBLE
+fieldspec[11].arg1 ""
+fieldspec[11].maxlength 1048576
+fieldspec[11].fieldtype INDEX
+fieldspec[12].name "c.f1"
+fieldspec[12].searchmethod AUTOUTF8
+fieldspec[12].arg1 ""
+fieldspec[12].maxlength 1048576
+fieldspec[12].fieldtype ATTRIBUTE
+fieldspec[13].name "c.f1s"
+fieldspec[13].searchmethod AUTOUTF8
+fieldspec[13].arg1 "prefix"
+fieldspec[13].maxlength 1048576
+fieldspec[13].fieldtype INDEX
+fieldspec[14].name "c.f3"
+fieldspec[14].searchmethod DOUBLE
+fieldspec[14].arg1 ""
+fieldspec[14].maxlength 1048576
+fieldspec[14].fieldtype INDEX
+fieldspec[15].name "c2.f1"
+fieldspec[15].searchmethod AUTOUTF8
+fieldspec[15].arg1 ""
+fieldspec[15].maxlength 1048576
+fieldspec[15].fieldtype ATTRIBUTE
+fieldspec[16].name "c2.f1s"
+fieldspec[16].searchmethod AUTOUTF8
+fieldspec[16].arg1 "suffix"
+fieldspec[16].maxlength 1048576
+fieldspec[16].fieldtype INDEX
+fieldspec[17].name "c2.f2"
+fieldspec[17].searchmethod INT32
+fieldspec[17].arg1 ""
+fieldspec[17].maxlength 1048576
+fieldspec[17].fieldtype INDEX
+fieldspec[18].name "c2.f3"
+fieldspec[18].searchmethod DOUBLE
+fieldspec[18].arg1 ""
+fieldspec[18].maxlength 1048576
+fieldspec[18].fieldtype INDEX
+fieldspec[19].name "c3.f1"
+fieldspec[19].searchmethod AUTOUTF8
+fieldspec[19].arg1 ""
+fieldspec[19].maxlength 1048576
+fieldspec[19].fieldtype ATTRIBUTE
+fieldspec[20].name "c3.f1s"
+fieldspec[20].searchmethod AUTOUTF8
+fieldspec[20].arg1 "prefix"
+fieldspec[20].maxlength 1048576
+fieldspec[20].fieldtype INDEX
+fieldspec[21].name "c3.f2"
+fieldspec[21].searchmethod INT32
+fieldspec[21].arg1 ""
+fieldspec[21].maxlength 1048576
+fieldspec[21].fieldtype INDEX
+fieldspec[22].name "c3.f3"
+fieldspec[22].searchmethod DOUBLE
+fieldspec[22].arg1 ""
+fieldspec[22].maxlength 1048576
+fieldspec[22].fieldtype INDEX
+fieldspec[23].name "n.nf1.f1"
+fieldspec[23].searchmethod AUTOUTF8
+fieldspec[23].arg1 ""
+fieldspec[23].maxlength 1048576
+fieldspec[23].fieldtype INDEX
+fieldspec[24].name "n.nf1.f1s"
+fieldspec[24].searchmethod AUTOUTF8
+fieldspec[24].arg1 "prefix"
+fieldspec[24].maxlength 1048576
+fieldspec[24].fieldtype INDEX
+fieldspec[25].name "n.nf1.f3"
+fieldspec[25].searchmethod DOUBLE
+fieldspec[25].arg1 ""
+fieldspec[25].maxlength 1048576
+fieldspec[25].fieldtype INDEX
+fieldspec[26].name "n.nf1s.f1"
+fieldspec[26].searchmethod AUTOUTF8
+fieldspec[26].arg1 "substring"
+fieldspec[26].maxlength 1048576
+fieldspec[26].fieldtype INDEX
+fieldspec[27].name "n.nf1s.f1s"
+fieldspec[27].searchmethod AUTOUTF8
+fieldspec[27].arg1 "substring"
+fieldspec[27].maxlength 1048576
+fieldspec[27].fieldtype INDEX
+fieldspec[28].name "n.nf1s.f2"
+fieldspec[28].searchmethod INT32
+fieldspec[28].arg1 ""
+fieldspec[28].maxlength 1048576
+fieldspec[28].fieldtype INDEX
+fieldspec[29].name "n.nf1s.f3"
+fieldspec[29].searchmethod DOUBLE
+fieldspec[29].arg1 ""
+fieldspec[29].maxlength 1048576
+fieldspec[29].fieldtype INDEX
+fieldspec[30].name "n.nf2"
+fieldspec[30].searchmethod AUTOUTF8
+fieldspec[30].arg1 ""
+fieldspec[30].maxlength 1048576
+fieldspec[30].fieldtype INDEX
+fieldspec[31].name "array1.f1"
+fieldspec[31].searchmethod AUTOUTF8
+fieldspec[31].arg1 ""
+fieldspec[31].maxlength 1048576
+fieldspec[31].fieldtype INDEX
+fieldspec[32].name "array1.f1s"
+fieldspec[32].searchmethod AUTOUTF8
+fieldspec[32].arg1 "substring"
+fieldspec[32].maxlength 1048576
+fieldspec[32].fieldtype INDEX
+fieldspec[33].name "array1.f2"
+fieldspec[33].searchmethod INT32
+fieldspec[33].arg1 ""
+fieldspec[33].maxlength 1048576
+fieldspec[33].fieldtype INDEX
+fieldspec[34].name "array1.f3"
+fieldspec[34].searchmethod DOUBLE
+fieldspec[34].arg1 ""
+fieldspec[34].maxlength 1048576
+fieldspec[34].fieldtype INDEX
+fieldspec[35].name "array2.f1"
+fieldspec[35].searchmethod AUTOUTF8
+fieldspec[35].arg1 ""
+fieldspec[35].maxlength 1048576
+fieldspec[35].fieldtype INDEX
+fieldspec[36].name "array2.f1s"
+fieldspec[36].searchmethod AUTOUTF8
+fieldspec[36].arg1 "substring"
+fieldspec[36].maxlength 1048576
+fieldspec[36].fieldtype INDEX
+fieldspec[37].name "array2.f2"
+fieldspec[37].searchmethod INT32
+fieldspec[37].arg1 ""
+fieldspec[37].maxlength 1048576
+fieldspec[37].fieldtype INDEX
+fieldspec[38].name "array2.f3"
+fieldspec[38].searchmethod DOUBLE
+fieldspec[38].arg1 ""
+fieldspec[38].maxlength 1048576
+fieldspec[38].fieldtype INDEX
+fieldspec[39].name "array3.f1"
+fieldspec[39].searchmethod AUTOUTF8
+fieldspec[39].arg1 ""
+fieldspec[39].maxlength 1048576
+fieldspec[39].fieldtype ATTRIBUTE
+fieldspec[40].name "array3.f1s"
+fieldspec[40].searchmethod AUTOUTF8
+fieldspec[40].arg1 "prefix"
+fieldspec[40].maxlength 1048576
+fieldspec[40].fieldtype INDEX
+fieldspec[41].name "array3.f3"
+fieldspec[41].searchmethod DOUBLE
+fieldspec[41].arg1 ""
+fieldspec[41].maxlength 1048576
+fieldspec[41].fieldtype INDEX
+fieldspec[42].name "subject.f1"
+fieldspec[42].searchmethod AUTOUTF8
+fieldspec[42].arg1 ""
+fieldspec[42].maxlength 1048576
+fieldspec[42].fieldtype INDEX
+fieldspec[43].name "d.f1"
+fieldspec[43].searchmethod AUTOUTF8
+fieldspec[43].arg1 "prefix"
+fieldspec[43].maxlength 1048576
+fieldspec[43].fieldtype INDEX
+fieldspec[44].name "d.f1s"
+fieldspec[44].searchmethod AUTOUTF8
+fieldspec[44].arg1 "prefix"
+fieldspec[44].maxlength 1048576
+fieldspec[44].fieldtype INDEX
+fieldspec[45].name "d.f2"
+fieldspec[45].searchmethod INT32
+fieldspec[45].arg1 ""
+fieldspec[45].maxlength 1048576
+fieldspec[45].fieldtype INDEX
+fieldspec[46].name "d.f3"
+fieldspec[46].searchmethod DOUBLE
+fieldspec[46].arg1 ""
+fieldspec[46].maxlength 1048576
+fieldspec[46].fieldtype INDEX
+fieldspec[47].name "e.f1"
+fieldspec[47].searchmethod AUTOUTF8
+fieldspec[47].arg1 "substring"
+fieldspec[47].maxlength 1048576
+fieldspec[47].fieldtype INDEX
+fieldspec[48].name "e.f1s"
+fieldspec[48].searchmethod AUTOUTF8
+fieldspec[48].arg1 "substring"
+fieldspec[48].maxlength 1048576
+fieldspec[48].fieldtype INDEX
+fieldspec[49].name "e.f2"
+fieldspec[49].searchmethod INT32
+fieldspec[49].arg1 ""
+fieldspec[49].maxlength 1048576
+fieldspec[49].fieldtype INDEX
+fieldspec[50].name "e.f3"
+fieldspec[50].searchmethod DOUBLE
+fieldspec[50].arg1 ""
+fieldspec[50].maxlength 1048576
+fieldspec[50].fieldtype INDEX
+fieldspec[51].name "f.f1"
+fieldspec[51].searchmethod AUTOUTF8
+fieldspec[51].arg1 "suffix"
+fieldspec[51].maxlength 1048576
+fieldspec[51].fieldtype INDEX
+fieldspec[52].name "f.f1s"
+fieldspec[52].searchmethod AUTOUTF8
+fieldspec[52].arg1 "suffix"
+fieldspec[52].maxlength 1048576
+fieldspec[52].fieldtype INDEX
+fieldspec[53].name "f.f2"
+fieldspec[53].searchmethod INT32
+fieldspec[53].arg1 ""
+fieldspec[53].maxlength 1048576
+fieldspec[53].fieldtype INDEX
+fieldspec[54].name "f.f3"
+fieldspec[54].searchmethod DOUBLE
+fieldspec[54].arg1 ""
+fieldspec[54].maxlength 1048576
+fieldspec[54].fieldtype INDEX
+fieldspec[55].name "g"
+fieldspec[55].searchmethod AUTOUTF8
+fieldspec[55].arg1 ""
+fieldspec[55].maxlength 1048576
+fieldspec[55].fieldtype INDEX
+documenttype[0].name "streamingstruct"
+documenttype[0].index[0].name "coupleof"
+documenttype[0].index[0].field[0].name "coupleof"
+documenttype[0].index[1].name "normalfields"
+documenttype[0].index[1].field[0].name "normalfields"
+documenttype[0].index[2].name "b"
+documenttype[0].index[2].field[0].name "b.f1"
+documenttype[0].index[2].field[1].name "b.f1s"
+documenttype[0].index[2].field[2].name "b.f2"
+documenttype[0].index[2].field[3].name "b.f3"
+documenttype[0].index[3].name "b.f1"
+documenttype[0].index[3].field[0].name "b.f1"
+documenttype[0].index[4].name "b.f1s"
+documenttype[0].index[4].field[0].name "b.f1s"
+documenttype[0].index[5].name "b.f2"
+documenttype[0].index[5].field[0].name "b.f2"
+documenttype[0].index[6].name "b.f3"
+documenttype[0].index[6].field[0].name "b.f3"
+documenttype[0].index[7].name "c"
+documenttype[0].index[7].field[0].name "c.f1"
+documenttype[0].index[7].field[1].name "c.f1s"
+documenttype[0].index[7].field[2].name "c.f3"
+documenttype[0].index[8].name "c.f1"
+documenttype[0].index[8].field[0].name "c.f1"
+documenttype[0].index[9].name "c.f1s"
+documenttype[0].index[9].field[0].name "c.f1s"
+documenttype[0].index[10].name "c.f3"
+documenttype[0].index[10].field[0].name "c.f3"
+documenttype[0].index[11].name "c2"
+documenttype[0].index[11].field[0].name "c2.f1"
+documenttype[0].index[11].field[1].name "c2.f1s"
+documenttype[0].index[11].field[2].name "c2.f2"
+documenttype[0].index[11].field[3].name "c2.f3"
+documenttype[0].index[12].name "c2.f1"
+documenttype[0].index[12].field[0].name "c2.f1"
+documenttype[0].index[13].name "c2.f1s"
+documenttype[0].index[13].field[0].name "c2.f1s"
+documenttype[0].index[14].name "c2.f2"
+documenttype[0].index[14].field[0].name "c2.f2"
+documenttype[0].index[15].name "c2.f3"
+documenttype[0].index[15].field[0].name "c2.f3"
+documenttype[0].index[16].name "c3"
+documenttype[0].index[16].field[0].name "c3.f1"
+documenttype[0].index[16].field[1].name "c3.f1s"
+documenttype[0].index[16].field[2].name "c3.f2"
+documenttype[0].index[16].field[3].name "c3.f3"
+documenttype[0].index[17].name "c3.f1"
+documenttype[0].index[17].field[0].name "c3.f1"
+documenttype[0].index[18].name "c3.f1s"
+documenttype[0].index[18].field[0].name "c3.f1s"
+documenttype[0].index[19].name "c3.f2"
+documenttype[0].index[19].field[0].name "c3.f2"
+documenttype[0].index[20].name "c3.f3"
+documenttype[0].index[20].field[0].name "c3.f3"
+documenttype[0].index[21].name "n"
+documenttype[0].index[21].field[0].name "n.nf1.f1"
+documenttype[0].index[21].field[1].name "n.nf1.f1s"
+documenttype[0].index[21].field[2].name "n.nf1.f3"
+documenttype[0].index[21].field[3].name "n.nf1s.f1"
+documenttype[0].index[21].field[4].name "n.nf1s.f1s"
+documenttype[0].index[21].field[5].name "n.nf1s.f2"
+documenttype[0].index[21].field[6].name "n.nf1s.f3"
+documenttype[0].index[21].field[7].name "n.nf2"
+documenttype[0].index[22].name "n.nf1"
+documenttype[0].index[22].field[0].name "n.nf1.f1"
+documenttype[0].index[22].field[1].name "n.nf1.f1s"
+documenttype[0].index[22].field[2].name "n.nf1.f3"
+documenttype[0].index[23].name "n.nf1.f1"
+documenttype[0].index[23].field[0].name "n.nf1.f1"
+documenttype[0].index[24].name "n.nf1.f1s"
+documenttype[0].index[24].field[0].name "n.nf1.f1s"
+documenttype[0].index[25].name "n.nf1.f3"
+documenttype[0].index[25].field[0].name "n.nf1.f3"
+documenttype[0].index[26].name "n.nf1s"
+documenttype[0].index[26].field[0].name "n.nf1s.f1"
+documenttype[0].index[26].field[1].name "n.nf1s.f1s"
+documenttype[0].index[26].field[2].name "n.nf1s.f2"
+documenttype[0].index[26].field[3].name "n.nf1s.f3"
+documenttype[0].index[27].name "n.nf1s.f1"
+documenttype[0].index[27].field[0].name "n.nf1s.f1"
+documenttype[0].index[28].name "n.nf1s.f1s"
+documenttype[0].index[28].field[0].name "n.nf1s.f1s"
+documenttype[0].index[29].name "n.nf1s.f2"
+documenttype[0].index[29].field[0].name "n.nf1s.f2"
+documenttype[0].index[30].name "n.nf1s.f3"
+documenttype[0].index[30].field[0].name "n.nf1s.f3"
+documenttype[0].index[31].name "n.nf2"
+documenttype[0].index[31].field[0].name "n.nf2"
+documenttype[0].index[32].name "array2"
+documenttype[0].index[32].field[0].name "array2.f1"
+documenttype[0].index[32].field[1].name "array2.f1s"
+documenttype[0].index[32].field[2].name "array2.f2"
+documenttype[0].index[32].field[3].name "array2.f3"
+documenttype[0].index[33].name "array2.f1"
+documenttype[0].index[33].field[0].name "array2.f1"
+documenttype[0].index[34].name "array2.f1s"
+documenttype[0].index[34].field[0].name "array2.f1s"
+documenttype[0].index[35].name "array2.f2"
+documenttype[0].index[35].field[0].name "array2.f2"
+documenttype[0].index[36].name "array2.f3"
+documenttype[0].index[36].field[0].name "array2.f3"
+documenttype[0].index[37].name "array3"
+documenttype[0].index[37].field[0].name "array3.f1"
+documenttype[0].index[37].field[1].name "array3.f1s"
+documenttype[0].index[37].field[2].name "array3.f3"
+documenttype[0].index[38].name "array3.f1"
+documenttype[0].index[38].field[0].name "array3.f1"
+documenttype[0].index[39].name "array3.f1s"
+documenttype[0].index[39].field[0].name "array3.f1s"
+documenttype[0].index[40].name "array3.f3"
+documenttype[0].index[40].field[0].name "array3.f3"
+documenttype[0].index[41].name "d"
+documenttype[0].index[41].field[0].name "d.f1"
+documenttype[0].index[41].field[1].name "d.f1s"
+documenttype[0].index[41].field[2].name "d.f2"
+documenttype[0].index[41].field[3].name "d.f3"
+documenttype[0].index[42].name "d.f1"
+documenttype[0].index[42].field[0].name "d.f1"
+documenttype[0].index[43].name "d.f1s"
+documenttype[0].index[43].field[0].name "d.f1s"
+documenttype[0].index[44].name "d.f2"
+documenttype[0].index[44].field[0].name "d.f2"
+documenttype[0].index[45].name "d.f3"
+documenttype[0].index[45].field[0].name "d.f3"
+documenttype[0].index[46].name "e"
+documenttype[0].index[46].field[0].name "e.f1"
+documenttype[0].index[46].field[1].name "e.f1s"
+documenttype[0].index[46].field[2].name "e.f2"
+documenttype[0].index[46].field[3].name "e.f3"
+documenttype[0].index[47].name "e.f1"
+documenttype[0].index[47].field[0].name "e.f1"
+documenttype[0].index[48].name "e.f1s"
+documenttype[0].index[48].field[0].name "e.f1s"
+documenttype[0].index[49].name "e.f2"
+documenttype[0].index[49].field[0].name "e.f2"
+documenttype[0].index[50].name "e.f3"
+documenttype[0].index[50].field[0].name "e.f3"
+documenttype[0].index[51].name "f"
+documenttype[0].index[51].field[0].name "f.f1"
+documenttype[0].index[51].field[1].name "f.f1s"
+documenttype[0].index[51].field[2].name "f.f2"
+documenttype[0].index[51].field[3].name "f.f3"
+documenttype[0].index[52].name "f.f1"
+documenttype[0].index[52].field[0].name "f.f1"
+documenttype[0].index[53].name "f.f1s"
+documenttype[0].index[53].field[0].name "f.f1s"
+documenttype[0].index[54].name "f.f2"
+documenttype[0].index[54].field[0].name "f.f2"
+documenttype[0].index[55].name "f.f3"
+documenttype[0].index[55].field[0].name "f.f3"
+documenttype[0].index[56].name "g"
+documenttype[0].index[56].field[0].name "g" \ No newline at end of file
diff --git a/config-model/src/test/derived/streamingstruct/vsmsummary.cfg b/config-model/src/test/derived/streamingstruct/vsmsummary.cfg
new file mode 100644
index 00000000000..b7a75932f5c
--- /dev/null
+++ b/config-model/src/test/derived/streamingstruct/vsmsummary.cfg
@@ -0,0 +1,83 @@
+outputclass ""
+fieldmap[0].summary "coupleof"
+fieldmap[0].document[0].field "coupleof"
+fieldmap[0].command NONE
+fieldmap[1].summary "anothersummaryfield"
+fieldmap[1].document[0].field "normalfields"
+fieldmap[1].command NONE
+fieldmap[2].summary "a"
+fieldmap[2].document[0].field "a.f1"
+fieldmap[2].document[1].field "a.f1s"
+fieldmap[2].document[2].field "a.f2"
+fieldmap[2].document[3].field "a.f3"
+fieldmap[2].command NONE
+fieldmap[3].summary "m"
+fieldmap[3].document[0].field "m"
+fieldmap[3].command NONE
+fieldmap[4].summary "b"
+fieldmap[4].document[0].field "b.f1"
+fieldmap[4].document[1].field "b.f1s"
+fieldmap[4].document[2].field "b.f2"
+fieldmap[4].document[3].field "b.f3"
+fieldmap[4].command NONE
+fieldmap[5].summary "c"
+fieldmap[5].document[0].field "c.f1"
+fieldmap[5].document[1].field "c.f1s"
+fieldmap[5].document[2].field "c.f3"
+fieldmap[5].command NONE
+fieldmap[6].summary "c2"
+fieldmap[6].document[0].field "c2.f1"
+fieldmap[6].document[1].field "c2.f1s"
+fieldmap[6].document[2].field "c2.f2"
+fieldmap[6].document[3].field "c2.f3"
+fieldmap[6].command NONE
+fieldmap[7].summary "c3"
+fieldmap[7].document[0].field "c3.f1"
+fieldmap[7].document[1].field "c3.f2"
+fieldmap[7].document[2].field "c3.f3"
+fieldmap[7].command NONE
+fieldmap[8].summary "n"
+fieldmap[8].document[0].field "n.nf1.f1"
+fieldmap[8].document[1].field "n.nf1.f1s"
+fieldmap[8].document[2].field "n.nf1s.f1"
+fieldmap[8].document[3].field "n.nf1s.f1s"
+fieldmap[8].document[4].field "n.nf1s.f2"
+fieldmap[8].document[5].field "n.nf1s.f3"
+fieldmap[8].document[6].field "n.nf2"
+fieldmap[8].command NONE
+fieldmap[9].summary "array1"
+fieldmap[9].document[0].field "array1.f1"
+fieldmap[9].document[1].field "array1.f1s"
+fieldmap[9].document[2].field "array1.f2"
+fieldmap[9].document[3].field "array1.f3"
+fieldmap[9].command NONE
+fieldmap[10].summary "array2"
+fieldmap[10].document[0].field "array2.f1"
+fieldmap[10].document[1].field "array2.f1s"
+fieldmap[10].document[2].field "array2.f2"
+fieldmap[10].document[3].field "array2.f3"
+fieldmap[10].command NONE
+fieldmap[11].summary "array3"
+fieldmap[11].document[0].field "array3.f1"
+fieldmap[11].document[1].field "array3.f1s"
+fieldmap[11].document[2].field "array3.f3"
+fieldmap[11].command NONE
+fieldmap[12].summary "subject"
+fieldmap[12].document[0].field "subject.f1"
+fieldmap[12].command NONE
+fieldmap[13].summary "g"
+fieldmap[13].document[0].field "g"
+fieldmap[13].command FLATTENJUNIPER
+fieldmap[14].summary "rankfeatures"
+fieldmap[14].command NONE
+fieldmap[15].summary "summaryfeatures"
+fieldmap[15].command NONE
+fieldmap[16].summary "snippet"
+fieldmap[16].document[0].field "a.f1"
+fieldmap[16].document[1].field "b.f2"
+fieldmap[16].command FLATTENJUNIPER
+fieldmap[17].summary "snippet2"
+fieldmap[17].document[0].field "a.f1"
+fieldmap[17].document[1].field "b.f1"
+fieldmap[17].document[2].field "b.f2"
+fieldmap[17].command FLATTENSPACE \ No newline at end of file
diff --git a/config-model/src/test/derived/streamingstructdefault/streamingstructdefault.sd b/config-model/src/test/derived/streamingstructdefault/streamingstructdefault.sd
new file mode 100644
index 00000000000..5b2c3cc5598
--- /dev/null
+++ b/config-model/src/test/derived/streamingstructdefault/streamingstructdefault.sd
@@ -0,0 +1,24 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search streamingstructdefault {
+ document streamingstructdefault {
+ struct sct {
+ field s1 type string {}
+ field s2 type string {}
+ }
+ field f1 type array<string> {
+ indexing: index | summary
+ summary-to: default
+ header
+ }
+ field f2 type array<sct> {
+ indexing: index | summary
+ header
+ }
+ }
+ document-summary default {
+ summary sum1 type string {
+ source: f1, f2.s1
+ dynamic
+ }
+ }
+}
diff --git a/config-model/src/test/derived/streamingstructdefault/summary.cfg b/config-model/src/test/derived/streamingstructdefault/summary.cfg
new file mode 100644
index 00000000000..288de48b11d
--- /dev/null
+++ b/config-model/src/test/derived/streamingstructdefault/summary.cfg
@@ -0,0 +1,15 @@
+defaultsummaryid 718801936
+classes[0].id 718801936
+classes[0].name "default"
+classes[0].fields[0].name "sum1"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "f1"
+classes[0].fields[1].type "jsonstring"
+classes[0].fields[2].name "f2"
+classes[0].fields[2].type "jsonstring"
+classes[0].fields[3].name "rankfeatures"
+classes[0].fields[3].type "featuredata"
+classes[0].fields[4].name "summaryfeatures"
+classes[0].fields[4].type "featuredata"
+classes[0].fields[5].name "documentid"
+classes[0].fields[5].type "longstring" \ No newline at end of file
diff --git a/config-model/src/test/derived/structanyorder/documentmanager.cfg b/config-model/src/test/derived/structanyorder/documentmanager.cfg
new file mode 100755
index 00000000000..dc56ed1cfc9
--- /dev/null
+++ b/config-model/src/test/derived/structanyorder/documentmanager.cfg
@@ -0,0 +1,83 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 109267174
+datatype[1].structtype[0].name "sct"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "s1"
+datatype[1].structtype[0].field[0].datatype 2
+datatype[1].structtype[0].field[1].name "s2"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "s3"
+datatype[1].structtype[0].field[2].datatype 109267174
+datatype[1].structtype[0].field[3].name "s4"
+datatype[1].structtype[0].field[3].datatype 97614088
+datatype[2].id 97614088
+datatype[2].structtype[0].name "foo"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "s1"
+datatype[2].structtype[0].field[0].datatype 0
+datatype[3].id 517946310
+datatype[3].structtype[0].name "annotation.banana"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "b"
+datatype[3].structtype[0].field[0].datatype 109267174
+datatype[3].structtype[0].field[1].name "c"
+datatype[3].structtype[0].field[1].datatype 97614088
+datatype[4].id -1244829667
+datatype[4].arraytype[0].datatype 109267174
+datatype[5].id -364910881
+datatype[5].structtype[0].name "annotationsimplicitstruct.header"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "structfield"
+datatype[5].structtype[0].field[0].datatype 109267174
+datatype[5].structtype[0].field[1].name "structarrayfield"
+datatype[5].structtype[0].field[1].datatype -1244829667
+datatype[5].structtype[0].field[2].name "rankfeatures"
+datatype[5].structtype[0].field[2].datatype 2
+datatype[5].structtype[0].field[3].name "summaryfeatures"
+datatype[5].structtype[0].field[3].datatype 2
+datatype[6].id -1503592268
+datatype[6].structtype[0].name "annotationsimplicitstruct.body"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[7].id -2099544992
+datatype[7].documenttype[0].name "annotationsimplicitstruct"
+datatype[7].documenttype[0].version 0
+datatype[7].documenttype[0].inherits[0].name "document"
+datatype[7].documenttype[0].inherits[0].version 0
+datatype[7].documenttype[0].headerstruct -364910881
+datatype[7].documenttype[0].bodystruct -1503592268
+datatype[7].documenttype[0].fieldsets{[document]}.fields[0] "structarrayfield"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[1] "structfield"
+annotationtype[0].id -269517759
+annotationtype[0].name "banana"
+annotationtype[0].datatype 517946310
diff --git a/config-model/src/test/derived/structanyorder/ilscripts.cfg b/config-model/src/test/derived/structanyorder/ilscripts.cfg
new file mode 100644
index 00000000000..b86571ce666
--- /dev/null
+++ b/config-model/src/test/derived/structanyorder/ilscripts.cfg
@@ -0,0 +1,6 @@
+maxtermoccurrences 100
+ilscript[0].doctype "annotationsimplicitstruct"
+ilscript[0].docfield[0] "structfield"
+ilscript[0].docfield[1] "structarrayfield"
+ilscript[0].content[0] "input structarrayfield | passthrough structarrayfield"
+ilscript[0].content[1] "input structfield | passthrough structfield" \ No newline at end of file
diff --git a/config-model/src/test/derived/structanyorder/index-info.cfg b/config-model/src/test/derived/structanyorder/index-info.cfg
new file mode 100755
index 00000000000..d62288df458
--- /dev/null
+++ b/config-model/src/test/derived/structanyorder/index-info.cfg
@@ -0,0 +1,247 @@
+indexinfo[0].name "annotationsimplicitstruct"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "structfield.s1"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "structfield.s2"
+indexinfo[0].command[3].command "index"
+indexinfo[0].command[4].indexname "structfield.s3.s1"
+indexinfo[0].command[4].command "index"
+indexinfo[0].command[5].indexname "structfield.s3.s2"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "structfield.s3.s3.s1"
+indexinfo[0].command[6].command "index"
+indexinfo[0].command[7].indexname "structfield.s3.s3.s2"
+indexinfo[0].command[7].command "index"
+indexinfo[0].command[8].indexname "structfield.s3.s3.s3.s1"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "structfield.s3.s3.s3.s2"
+indexinfo[0].command[9].command "index"
+indexinfo[0].command[10].indexname "structfield.s3.s3.s3.s3.s1"
+indexinfo[0].command[10].command "index"
+indexinfo[0].command[11].indexname "structfield.s3.s3.s3.s3.s2"
+indexinfo[0].command[11].command "index"
+indexinfo[0].command[12].indexname "structfield.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[12].command "index"
+indexinfo[0].command[13].indexname "structfield.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[13].command "index"
+indexinfo[0].command[14].indexname "structfield.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "structfield.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[15].command "index"
+indexinfo[0].command[16].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[16].command "index"
+indexinfo[0].command[17].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[17].command "index"
+indexinfo[0].command[18].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[18].command "index"
+indexinfo[0].command[19].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[19].command "index"
+indexinfo[0].command[20].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[20].command "index"
+indexinfo[0].command[21].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[22].command "index"
+indexinfo[0].command[23].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[23].command "index"
+indexinfo[0].command[24].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[24].command "index"
+indexinfo[0].command[25].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[25].command "index"
+indexinfo[0].command[26].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[26].command "numerical"
+indexinfo[0].command[27].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[27].command "index"
+indexinfo[0].command[28].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[28].command "index"
+indexinfo[0].command[29].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[29].command "index"
+indexinfo[0].command[30].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[30].command "numerical"
+indexinfo[0].command[31].indexname "structfield.s3.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[31].command "index"
+indexinfo[0].command[32].indexname "structfield.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[32].command "index"
+indexinfo[0].command[33].indexname "structfield.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[33].command "index"
+indexinfo[0].command[34].indexname "structfield.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[34].command "numerical"
+indexinfo[0].command[35].indexname "structfield.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[35].command "index"
+indexinfo[0].command[36].indexname "structfield.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[36].command "index"
+indexinfo[0].command[37].indexname "structfield.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[37].command "index"
+indexinfo[0].command[38].indexname "structfield.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[38].command "numerical"
+indexinfo[0].command[39].indexname "structfield.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[39].command "index"
+indexinfo[0].command[40].indexname "structfield.s3.s3.s3.s3.s3"
+indexinfo[0].command[40].command "index"
+indexinfo[0].command[41].indexname "structfield.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[41].command "index"
+indexinfo[0].command[42].indexname "structfield.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[42].command "numerical"
+indexinfo[0].command[43].indexname "structfield.s3.s3.s3.s3.s4"
+indexinfo[0].command[43].command "index"
+indexinfo[0].command[44].indexname "structfield.s3.s3.s3.s3"
+indexinfo[0].command[44].command "index"
+indexinfo[0].command[45].indexname "structfield.s3.s3.s3.s4.s1"
+indexinfo[0].command[45].command "index"
+indexinfo[0].command[46].indexname "structfield.s3.s3.s3.s4.s1"
+indexinfo[0].command[46].command "numerical"
+indexinfo[0].command[47].indexname "structfield.s3.s3.s3.s4"
+indexinfo[0].command[47].command "index"
+indexinfo[0].command[48].indexname "structfield.s3.s3.s3"
+indexinfo[0].command[48].command "index"
+indexinfo[0].command[49].indexname "structfield.s3.s3.s4.s1"
+indexinfo[0].command[49].command "index"
+indexinfo[0].command[50].indexname "structfield.s3.s3.s4.s1"
+indexinfo[0].command[50].command "numerical"
+indexinfo[0].command[51].indexname "structfield.s3.s3.s4"
+indexinfo[0].command[51].command "index"
+indexinfo[0].command[52].indexname "structfield.s3.s3"
+indexinfo[0].command[52].command "index"
+indexinfo[0].command[53].indexname "structfield.s3.s4.s1"
+indexinfo[0].command[53].command "index"
+indexinfo[0].command[54].indexname "structfield.s3.s4.s1"
+indexinfo[0].command[54].command "numerical"
+indexinfo[0].command[55].indexname "structfield.s3.s4"
+indexinfo[0].command[55].command "index"
+indexinfo[0].command[56].indexname "structfield.s3"
+indexinfo[0].command[56].command "index"
+indexinfo[0].command[57].indexname "structfield.s4.s1"
+indexinfo[0].command[57].command "index"
+indexinfo[0].command[58].indexname "structfield.s4.s1"
+indexinfo[0].command[58].command "numerical"
+indexinfo[0].command[59].indexname "structfield.s4"
+indexinfo[0].command[59].command "index"
+indexinfo[0].command[60].indexname "structfield"
+indexinfo[0].command[60].command "index"
+indexinfo[0].command[61].indexname "structarrayfield.s1"
+indexinfo[0].command[61].command "index"
+indexinfo[0].command[62].indexname "structarrayfield.s2"
+indexinfo[0].command[62].command "index"
+indexinfo[0].command[63].indexname "structarrayfield.s3.s1"
+indexinfo[0].command[63].command "index"
+indexinfo[0].command[64].indexname "structarrayfield.s3.s2"
+indexinfo[0].command[64].command "index"
+indexinfo[0].command[65].indexname "structarrayfield.s3.s3.s1"
+indexinfo[0].command[65].command "index"
+indexinfo[0].command[66].indexname "structarrayfield.s3.s3.s2"
+indexinfo[0].command[66].command "index"
+indexinfo[0].command[67].indexname "structarrayfield.s3.s3.s3.s1"
+indexinfo[0].command[67].command "index"
+indexinfo[0].command[68].indexname "structarrayfield.s3.s3.s3.s2"
+indexinfo[0].command[68].command "index"
+indexinfo[0].command[69].indexname "structarrayfield.s3.s3.s3.s3.s1"
+indexinfo[0].command[69].command "index"
+indexinfo[0].command[70].indexname "structarrayfield.s3.s3.s3.s3.s2"
+indexinfo[0].command[70].command "index"
+indexinfo[0].command[71].indexname "structarrayfield.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[71].command "index"
+indexinfo[0].command[72].indexname "structarrayfield.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[72].command "index"
+indexinfo[0].command[73].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[73].command "index"
+indexinfo[0].command[74].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[74].command "index"
+indexinfo[0].command[75].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[75].command "index"
+indexinfo[0].command[76].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[76].command "index"
+indexinfo[0].command[77].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[77].command "index"
+indexinfo[0].command[78].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[78].command "index"
+indexinfo[0].command[79].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s1"
+indexinfo[0].command[79].command "index"
+indexinfo[0].command[80].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s2"
+indexinfo[0].command[80].command "index"
+indexinfo[0].command[81].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[81].command "index"
+indexinfo[0].command[82].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[82].command "index"
+indexinfo[0].command[83].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[83].command "index"
+indexinfo[0].command[84].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[84].command "index"
+indexinfo[0].command[85].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[85].command "numerical"
+indexinfo[0].command[86].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[86].command "index"
+indexinfo[0].command[87].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[87].command "index"
+indexinfo[0].command[88].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[88].command "index"
+indexinfo[0].command[89].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[89].command "numerical"
+indexinfo[0].command[90].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[90].command "index"
+indexinfo[0].command[91].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[91].command "index"
+indexinfo[0].command[92].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[92].command "index"
+indexinfo[0].command[93].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[93].command "numerical"
+indexinfo[0].command[94].indexname "structarrayfield.s3.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[94].command "index"
+indexinfo[0].command[95].indexname "structarrayfield.s3.s3.s3.s3.s3.s3"
+indexinfo[0].command[95].command "index"
+indexinfo[0].command[96].indexname "structarrayfield.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[96].command "index"
+indexinfo[0].command[97].indexname "structarrayfield.s3.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[97].command "numerical"
+indexinfo[0].command[98].indexname "structarrayfield.s3.s3.s3.s3.s3.s4"
+indexinfo[0].command[98].command "index"
+indexinfo[0].command[99].indexname "structarrayfield.s3.s3.s3.s3.s3"
+indexinfo[0].command[99].command "index"
+indexinfo[0].command[100].indexname "structarrayfield.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[100].command "index"
+indexinfo[0].command[101].indexname "structarrayfield.s3.s3.s3.s3.s4.s1"
+indexinfo[0].command[101].command "numerical"
+indexinfo[0].command[102].indexname "structarrayfield.s3.s3.s3.s3.s4"
+indexinfo[0].command[102].command "index"
+indexinfo[0].command[103].indexname "structarrayfield.s3.s3.s3.s3"
+indexinfo[0].command[103].command "index"
+indexinfo[0].command[104].indexname "structarrayfield.s3.s3.s3.s4.s1"
+indexinfo[0].command[104].command "index"
+indexinfo[0].command[105].indexname "structarrayfield.s3.s3.s3.s4.s1"
+indexinfo[0].command[105].command "numerical"
+indexinfo[0].command[106].indexname "structarrayfield.s3.s3.s3.s4"
+indexinfo[0].command[106].command "index"
+indexinfo[0].command[107].indexname "structarrayfield.s3.s3.s3"
+indexinfo[0].command[107].command "index"
+indexinfo[0].command[108].indexname "structarrayfield.s3.s3.s4.s1"
+indexinfo[0].command[108].command "index"
+indexinfo[0].command[109].indexname "structarrayfield.s3.s3.s4.s1"
+indexinfo[0].command[109].command "numerical"
+indexinfo[0].command[110].indexname "structarrayfield.s3.s3.s4"
+indexinfo[0].command[110].command "index"
+indexinfo[0].command[111].indexname "structarrayfield.s3.s3"
+indexinfo[0].command[111].command "index"
+indexinfo[0].command[112].indexname "structarrayfield.s3.s4.s1"
+indexinfo[0].command[112].command "index"
+indexinfo[0].command[113].indexname "structarrayfield.s3.s4.s1"
+indexinfo[0].command[113].command "numerical"
+indexinfo[0].command[114].indexname "structarrayfield.s3.s4"
+indexinfo[0].command[114].command "index"
+indexinfo[0].command[115].indexname "structarrayfield.s3"
+indexinfo[0].command[115].command "index"
+indexinfo[0].command[116].indexname "structarrayfield.s4.s1"
+indexinfo[0].command[116].command "index"
+indexinfo[0].command[117].indexname "structarrayfield.s4.s1"
+indexinfo[0].command[117].command "numerical"
+indexinfo[0].command[118].indexname "structarrayfield.s4"
+indexinfo[0].command[118].command "index"
+indexinfo[0].command[119].indexname "structarrayfield"
+indexinfo[0].command[119].command "index"
+indexinfo[0].command[120].indexname "structarrayfield"
+indexinfo[0].command[120].command "multivalue"
+indexinfo[0].command[121].indexname "rankfeatures"
+indexinfo[0].command[121].command "index"
+indexinfo[0].command[122].indexname "summaryfeatures"
+indexinfo[0].command[122].command "index" \ No newline at end of file
diff --git a/config-model/src/test/derived/structanyorder/structanyorder.sd b/config-model/src/test/derived/structanyorder/structanyorder.sd
new file mode 100755
index 00000000000..418c0985062
--- /dev/null
+++ b/config-model/src/test/derived/structanyorder/structanyorder.sd
@@ -0,0 +1,27 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search annotationsimplicitstruct {
+
+ document annotationsimplicitstruct {
+ field structfield type sct {
+ }
+
+ field structarrayfield type array<sct> {
+ }
+
+ annotation banana {
+ field b type sct { }
+ field c type foo { }
+ }
+
+ struct sct {
+ field s1 type string {}
+ field s2 type string {}
+ field s3 type sct {}
+ field s4 type foo {}
+ }
+
+ struct foo {
+ field s1 type int {}
+ }
+ }
+}
diff --git a/config-model/src/test/derived/tensor/attributes.cfg b/config-model/src/test/derived/tensor/attributes.cfg
new file mode 100644
index 00000000000..a3026d579d5
--- /dev/null
+++ b/config-model/src/test/derived/tensor/attributes.cfg
@@ -0,0 +1,57 @@
+attribute[0].name "f2"
+attribute[0].datatype TENSOR
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "f3"
+attribute[1].datatype TENSOR
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "f4"
+attribute[2].datatype TENSOR
+attribute[2].collectiontype SINGLE
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending true
+attribute[2].sortfunction UCA
+attribute[2].sortstrength PRIMARY
+attribute[2].sortlocale ""
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype "tensor(x[10],y[20])" \ No newline at end of file
diff --git a/config-model/src/test/derived/tensor/documenttypes.cfg b/config-model/src/test/derived/tensor/documenttypes.cfg
new file mode 100644
index 00000000000..2a76458a97a
--- /dev/null
+++ b/config-model/src/test/derived/tensor/documenttypes.cfg
@@ -0,0 +1,65 @@
+enablecompression false
+documenttype[0].id -1290043429
+documenttype[0].name "tensor"
+documenttype[0].version 0
+documenttype[0].headerstruct 2125927172
+documenttype[0].bodystruct -1903234535
+documenttype[0].inherits[0].id 8
+documenttype[0].datatype[0].id 2125927172
+documenttype[0].datatype[0].type STRUCT
+documenttype[0].datatype[0].array.element.id 0
+documenttype[0].datatype[0].map.key.id 0
+documenttype[0].datatype[0].map.value.id 0
+documenttype[0].datatype[0].wset.key.id 0
+documenttype[0].datatype[0].wset.createifnonexistent false
+documenttype[0].datatype[0].wset.removeifzero false
+documenttype[0].datatype[0].annotationref.annotation.id 0
+documenttype[0].datatype[0].sstruct.name "tensor.header"
+documenttype[0].datatype[0].sstruct.version 0
+documenttype[0].datatype[0].sstruct.compression.type NONE
+documenttype[0].datatype[0].sstruct.compression.level 0
+documenttype[0].datatype[0].sstruct.compression.threshold 95
+documenttype[0].datatype[0].sstruct.compression.minsize 200
+documenttype[0].datatype[0].sstruct.field[0].name "f1"
+documenttype[0].datatype[0].sstruct.field[0].id 26661415
+documenttype[0].datatype[0].sstruct.field[0].id_v6 1740179945
+documenttype[0].datatype[0].sstruct.field[0].datatype 21
+documenttype[0].datatype[0].sstruct.field[1].name "f2"
+documenttype[0].datatype[0].sstruct.field[1].id 2080644671
+documenttype[0].datatype[0].sstruct.field[1].id_v6 1424572148
+documenttype[0].datatype[0].sstruct.field[1].datatype 21
+documenttype[0].datatype[0].sstruct.field[2].name "f3"
+documenttype[0].datatype[0].sstruct.field[2].id 1295091863
+documenttype[0].datatype[0].sstruct.field[2].id_v6 1444109654
+documenttype[0].datatype[0].sstruct.field[2].datatype 21
+documenttype[0].datatype[0].sstruct.field[3].name "f4"
+documenttype[0].datatype[0].sstruct.field[3].id 1224191509
+documenttype[0].datatype[0].sstruct.field[3].id_v6 1039544782
+documenttype[0].datatype[0].sstruct.field[3].datatype 21
+documenttype[0].datatype[0].sstruct.field[4].name "rankfeatures"
+documenttype[0].datatype[0].sstruct.field[4].id 1883197392
+documenttype[0].datatype[0].sstruct.field[4].id_v6 699950698
+documenttype[0].datatype[0].sstruct.field[4].datatype 2
+documenttype[0].datatype[0].sstruct.field[5].name "summaryfeatures"
+documenttype[0].datatype[0].sstruct.field[5].id 1840337115
+documenttype[0].datatype[0].sstruct.field[5].id_v6 1981648971
+documenttype[0].datatype[0].sstruct.field[5].datatype 2
+documenttype[0].datatype[1].id -1903234535
+documenttype[0].datatype[1].type STRUCT
+documenttype[0].datatype[1].array.element.id 0
+documenttype[0].datatype[1].map.key.id 0
+documenttype[0].datatype[1].map.value.id 0
+documenttype[0].datatype[1].wset.key.id 0
+documenttype[0].datatype[1].wset.createifnonexistent false
+documenttype[0].datatype[1].wset.removeifzero false
+documenttype[0].datatype[1].annotationref.annotation.id 0
+documenttype[0].datatype[1].sstruct.name "tensor.body"
+documenttype[0].datatype[1].sstruct.version 0
+documenttype[0].datatype[1].sstruct.compression.type NONE
+documenttype[0].datatype[1].sstruct.compression.level 0
+documenttype[0].datatype[1].sstruct.compression.threshold 95
+documenttype[0].datatype[1].sstruct.compression.minsize 200
+documenttype[0].fieldsets{[document]}.fields[0] "f1"
+documenttype[0].fieldsets{[document]}.fields[1] "f2"
+documenttype[0].fieldsets{[document]}.fields[2] "f3"
+documenttype[0].fieldsets{[document]}.fields[3] "f4"
diff --git a/config-model/src/test/derived/tensor/summary.cfg b/config-model/src/test/derived/tensor/summary.cfg
new file mode 100644
index 00000000000..b6d00bc4799
--- /dev/null
+++ b/config-model/src/test/derived/tensor/summary.cfg
@@ -0,0 +1,25 @@
+defaultsummaryid 657634105
+classes[0].id 657634105
+classes[0].name "default"
+classes[0].fields[0].name "f1"
+classes[0].fields[0].type "jsonstring"
+classes[0].fields[1].name "f3"
+classes[0].fields[1].type "jsonstring"
+classes[0].fields[2].name "rankfeatures"
+classes[0].fields[2].type "featuredata"
+classes[0].fields[3].name "summaryfeatures"
+classes[0].fields[3].type "featuredata"
+classes[0].fields[4].name "documentid"
+classes[0].fields[4].type "longstring"
+classes[1].id 457955124
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "f2"
+classes[1].fields[0].type "jsonstring"
+classes[1].fields[1].name "f3"
+classes[1].fields[1].type "jsonstring"
+classes[1].fields[2].name "f4"
+classes[1].fields[2].type "jsonstring"
+classes[1].fields[3].name "rankfeatures"
+classes[1].fields[3].type "featuredata"
+classes[1].fields[4].name "summaryfeatures"
+classes[1].fields[4].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/tensor/tensor.sd b/config-model/src/test/derived/tensor/tensor.sd
new file mode 100644
index 00000000000..b89b96e253b
--- /dev/null
+++ b/config-model/src/test/derived/tensor/tensor.sd
@@ -0,0 +1,18 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search tensor {
+ document tensor {
+ field f1 type tensor {
+ indexing: summary
+ }
+ field f2 type tensor {
+ indexing: attribute
+ }
+ field f3 type tensor {
+ indexing: attribute | summary
+ }
+ field f4 type tensor {
+ indexing: attribute
+ attribute: tensor(x[10],y[20])
+ }
+ }
+}
diff --git a/config-model/src/test/derived/twostreamingstructs/documentmanager.cfg b/config-model/src/test/derived/twostreamingstructs/documentmanager.cfg
new file mode 100644
index 00000000000..6dee91664be
--- /dev/null
+++ b/config-model/src/test/derived/twostreamingstructs/documentmanager.cfg
@@ -0,0 +1,166 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 105061838
+datatype[1].structtype[0].name "ns1"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "nf1"
+datatype[1].structtype[0].field[0].datatype 3474528
+datatype[1].structtype[0].field[1].name "nf1s"
+datatype[1].structtype[0].field[1].datatype 3474528
+datatype[1].structtype[0].field[2].name "nf2"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[2].id 3474528
+datatype[2].structtype[0].name "s1"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "f1"
+datatype[2].structtype[0].field[0].datatype 2
+datatype[2].structtype[0].field[1].name "f1s"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[2].structtype[0].field[2].name "f2"
+datatype[2].structtype[0].field[2].datatype 0
+datatype[2].structtype[0].field[3].name "f3"
+datatype[2].structtype[0].field[3].datatype 5
+datatype[3].id -1497802371
+datatype[3].maptype[0].keytype 4
+datatype[3].maptype[0].valtype 2
+datatype[4].id -1425630723
+datatype[4].arraytype[0].datatype 3474528
+datatype[5].id 731395686
+datatype[5].structtype[0].name "streamingstruct.header"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[5].structtype[0].field[0].name "coupleof"
+datatype[5].structtype[0].field[0].datatype 2
+datatype[5].structtype[0].field[1].name "normalfields"
+datatype[5].structtype[0].field[1].datatype 2
+datatype[5].structtype[0].field[2].name "a"
+datatype[5].structtype[0].field[2].datatype 3474528
+datatype[5].structtype[0].field[3].name "m"
+datatype[5].structtype[0].field[3].datatype -1497802371
+datatype[5].structtype[0].field[4].name "b"
+datatype[5].structtype[0].field[4].datatype 3474528
+datatype[5].structtype[0].field[5].name "c"
+datatype[5].structtype[0].field[5].datatype 3474528
+datatype[5].structtype[0].field[6].name "c2"
+datatype[5].structtype[0].field[6].datatype 3474528
+datatype[5].structtype[0].field[7].name "c3"
+datatype[5].structtype[0].field[7].datatype 3474528
+datatype[5].structtype[0].field[8].name "n"
+datatype[5].structtype[0].field[8].datatype 105061838
+datatype[5].structtype[0].field[9].name "array1"
+datatype[5].structtype[0].field[9].datatype -1425630723
+datatype[5].structtype[0].field[10].name "array2"
+datatype[5].structtype[0].field[10].datatype -1425630723
+datatype[5].structtype[0].field[11].name "array3"
+datatype[5].structtype[0].field[11].datatype -1425630723
+datatype[5].structtype[0].field[12].name "subject"
+datatype[5].structtype[0].field[12].datatype 3474528
+datatype[5].structtype[0].field[13].name "d"
+datatype[5].structtype[0].field[13].datatype 3474528
+datatype[5].structtype[0].field[14].name "e"
+datatype[5].structtype[0].field[14].datatype 3474528
+datatype[5].structtype[0].field[15].name "f"
+datatype[5].structtype[0].field[15].datatype 3474528
+datatype[5].structtype[0].field[16].name "g"
+datatype[5].structtype[0].field[16].datatype 2
+datatype[5].structtype[0].field[17].name "anothersummaryfield"
+datatype[5].structtype[0].field[17].datatype 2
+datatype[5].structtype[0].field[18].name "rankfeatures"
+datatype[5].structtype[0].field[18].datatype 2
+datatype[5].structtype[0].field[19].name "summaryfeatures"
+datatype[5].structtype[0].field[19].datatype 2
+datatype[5].structtype[0].field[20].name "snippet"
+datatype[5].structtype[0].field[20].datatype 2
+datatype[5].structtype[0].field[21].name "snippet2"
+datatype[5].structtype[0].field[21].datatype 2
+datatype[6].id 1858438651
+datatype[6].structtype[0].name "streamingstruct.body"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[7].id 1433175737
+datatype[7].documenttype[0].name "streamingstruct"
+datatype[7].documenttype[0].version 0
+datatype[7].documenttype[0].inherits[0].name "document"
+datatype[7].documenttype[0].inherits[0].version 0
+datatype[7].documenttype[0].headerstruct 731395686
+datatype[7].documenttype[0].bodystruct 1858438651
+datatype[7].documenttype[0].fieldsets{[document]}.fields[0] "a"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[1] "array1"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[2] "array2"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[3] "array3"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[4] "b"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[5] "c"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[6] "c2"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[7] "c3"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[8] "coupleof"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[9] "d"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[10] "e"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[11] "f"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[12] "g"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[13] "m"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[14] "n"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[15] "normalfields"
+datatype[7].documenttype[0].fieldsets{[document]}.fields[16] "subject"
+datatype[8].id -995681764
+datatype[8].structtype[0].name "pair"
+datatype[8].structtype[0].version 0
+datatype[8].structtype[0].compresstype NONE
+datatype[8].structtype[0].compresslevel 0
+datatype[8].structtype[0].compressthreshold 95
+datatype[8].structtype[0].compressminsize 800
+datatype[8].structtype[0].field[0].name "key"
+datatype[8].structtype[0].field[0].datatype 2
+datatype[8].structtype[0].field[1].name "value"
+datatype[8].structtype[0].field[1].datatype 2
+datatype[9].id 355471259
+datatype[9].structtype[0].name "whatever.header"
+datatype[9].structtype[0].version 0
+datatype[9].structtype[0].compresstype NONE
+datatype[9].structtype[0].compresslevel 0
+datatype[9].structtype[0].compressthreshold 95
+datatype[9].structtype[0].compressminsize 800
+datatype[9].structtype[0].field[0].name "rankfeatures"
+datatype[9].structtype[0].field[0].datatype 2
+datatype[9].structtype[0].field[1].name "summaryfeatures"
+datatype[9].structtype[0].field[1].datatype 2
+datatype[10].id -1417926544
+datatype[10].structtype[0].name "whatever.body"
+datatype[10].structtype[0].version 0
+datatype[10].structtype[0].compresstype NONE
+datatype[10].structtype[0].compresslevel 0
+datatype[10].structtype[0].compressthreshold 95
+datatype[10].structtype[0].compressminsize 800
+datatype[10].structtype[0].field[0].name "f1"
+datatype[10].structtype[0].field[0].datatype -995681764
+datatype[11].id -778211548
+datatype[11].documenttype[0].name "whatever"
+datatype[11].documenttype[0].version 0
+datatype[11].documenttype[0].inherits[0].name "document"
+datatype[11].documenttype[0].inherits[0].version 0
+datatype[11].documenttype[0].headerstruct 355471259
+datatype[11].documenttype[0].bodystruct -1417926544
+datatype[11].documenttype[0].fieldsets{[document]}.fields[0] "f1"
diff --git a/config-model/src/test/derived/twostreamingstructs/streamingstruct.sd b/config-model/src/test/derived/twostreamingstructs/streamingstruct.sd
new file mode 100644
index 00000000000..dabe9837af8
--- /dev/null
+++ b/config-model/src/test/derived/twostreamingstructs/streamingstruct.sd
@@ -0,0 +1,183 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search streamingstruct {
+
+ document streamingstruct {
+ field coupleof type string {
+ indexing: index | summary
+ }
+
+ field normalfields type string {
+ indexing: index | summary
+ summary anothersummaryfield {
+ source: normalfields
+ }
+ }
+
+ struct s1 {
+ field f1 type string { }
+ field f1s type string { match: substring }
+ field f2 type int { }
+ field f3 type double { }
+ # Allow default matchtypes in struct. Can be overridden.
+ # No index/attribute related stuff. It is only a datatype definition.
+ }
+ struct ns1 {
+ field nf1 type s1 { }
+ field nf1s type s1 { match: substring }
+ field nf2 type string { }
+ # May specify nested structs.
+ }
+
+ field a type s1 {
+ indexing: summary
+ # Will generate attribute a.f1, a.f1s, a.f2, a.f3
+ # with datatypes preserved.
+ # No customisation.
+ # a will be a synonym for all (a.*)
+ }
+ field m type map<long, string> {
+ indexing: summary
+ # Will generate attribute m.key, m.value
+ # with datatypes preserved.
+ # m will be a synonym for all (m.*)
+ }
+ field b type s1 {
+ indexing: index | summary
+ # Will generate index b.f1, b.f1s, b.f2, b.f3
+ # with datatypes preserved if backend allows.
+ # No customisation.
+ # b will be synonym for all (b.*).
+ }
+ field c type s1 {
+ struct-field f1 {
+ # Whatever you normally write in a field declaration
+ indexing: attribute | summary # -> Generates attribute c.f1
+ }
+ struct-field f1s {
+ indexing: index | summary # -> c.f1s
+ match: prefix
+ }
+ struct-field f3 {
+ indexing: index | summary # -> c.f3
+ }
+ # attribute c will be synonym for c.f1.
+ # Index c will be synonym for c.f1s OR c.f3.
+ # Indexed search can handle that however they want.
+ }
+ field c2 type s1 {
+ struct-field f1 {
+ # Whatever you normally write in a field declaration
+ indexing: attribute | summary # -> Generates attribute c2.f1
+ }
+ struct-field f1s {
+ indexing: index | summary # -> c2.f1s
+ match: suffix
+ }
+ struct-field f2 {
+ indexing: index | summary # -> c2.f2
+ }
+ struct-field f3 {
+ indexing: index | summary # -> c2.f3
+ }
+ }
+ field c3 type s1 {
+ # Uses all sub fields, but not summary for all.
+ struct-field f1 {
+ indexing: attribute | summary
+ }
+ struct-field f1s {
+ indexing: index
+ match: prefix
+ }
+ struct-field f2 {
+ indexing: index | summary
+ }
+ struct-field f3 {
+ indexing: index | summary
+ }
+ }
+ field n type ns1 {
+ struct-field nf1 {
+ struct-field f1 {
+ indexing: index | summary
+ }
+ struct-field f1s {
+ indexing: index | summary
+ match: prefix
+ }
+ struct-field f3 {
+ indexing: index
+ }
+ }
+ struct-field nf1s {
+ indexing: index | summary
+ }
+ struct-field nf2 {
+ indexing: index | summary
+ }
+ # Will generate indexes n.nf1.f1, n.nf1.f1s, n.nf1.f3,
+ # n.nf1s.f1, n.nf1s.f1s, n.nf1s.f2, n.nf1s.f3
+ # and n.nf2.
+ # n will be synonym for all 8,
+ # n.nf1 will be synonym for the first 3 and
+ # n.nf1s will be synonym for the next 4
+ }
+
+ field array1 type array<s1> {
+ indexing: summary
+ }
+ field array2 type array<s1> {
+ indexing: index | summary
+ }
+ field array3 type array<s1> {
+ struct-field f1 {
+ indexing: attribute | summary # -> Generates attribute array3.f1
+ }
+ struct-field f1s {
+ indexing: index | summary # -> array3.f1s
+ match: prefix
+ }
+ struct-field f3 {
+ indexing: index | summary # -> array3.f3
+ }
+ }
+ field subject type s1 {
+ struct-field f1 {
+ indexing: summary
+ summary subject {
+ source:subject.f1
+ }
+ }
+ }
+ field d type s1 {
+ indexing: index
+ # override matching for all subfields
+ match: prefix
+ }
+ field e type s1 {
+ indexing: index
+ # override matching for all subfields
+ match: substring
+ }
+ field f type s1 {
+ indexing: index
+ # override matching for all subfields
+ match: suffix
+ }
+ field g type string {
+ indexing: index | summary
+ summary: dynamic
+ }
+ }
+
+ document-summary summ {
+ summary snippet type string {
+ dynamic
+ source: a.f1, b.f2
+ }
+ summary snippet2 type string {
+ source: a.f1, b.f1, b.f2
+ }
+ }
+
+}
diff --git a/config-model/src/test/derived/twostreamingstructs/summary.cfg b/config-model/src/test/derived/twostreamingstructs/summary.cfg
new file mode 100644
index 00000000000..9d722116850
--- /dev/null
+++ b/config-model/src/test/derived/twostreamingstructs/summary.cfg
@@ -0,0 +1,51 @@
+defaultsummaryid 569269436
+classes[0].id 569269436
+classes[0].name "default"
+classes[0].fields[0].name "coupleof"
+classes[0].fields[0].type "longstring"
+classes[0].fields[1].name "anothersummaryfield"
+classes[0].fields[1].type "longstring"
+classes[0].fields[2].name "a"
+classes[0].fields[2].type "jsonstring"
+classes[0].fields[3].name "m"
+classes[0].fields[3].type "jsonstring"
+classes[0].fields[4].name "b"
+classes[0].fields[4].type "jsonstring"
+classes[0].fields[5].name "c"
+classes[0].fields[5].type "jsonstring"
+classes[0].fields[6].name "c2"
+classes[0].fields[6].type "jsonstring"
+classes[0].fields[7].name "c3"
+classes[0].fields[7].type "jsonstring"
+classes[0].fields[8].name "n"
+classes[0].fields[8].type "jsonstring"
+classes[0].fields[9].name "array1"
+classes[0].fields[9].type "jsonstring"
+classes[0].fields[10].name "array2"
+classes[0].fields[10].type "jsonstring"
+classes[0].fields[11].name "array3"
+classes[0].fields[11].type "jsonstring"
+classes[0].fields[12].name "subject"
+classes[0].fields[12].type "jsonstring"
+classes[0].fields[13].name "g"
+classes[0].fields[13].type "longstring"
+classes[0].fields[14].name "rankfeatures"
+classes[0].fields[14].type "featuredata"
+classes[0].fields[15].name "summaryfeatures"
+classes[0].fields[15].type "featuredata"
+classes[0].fields[16].name "snippet"
+classes[0].fields[16].type "longstring"
+classes[0].fields[17].name "snippet2"
+classes[0].fields[17].type "longstring"
+classes[0].fields[18].name "documentid"
+classes[0].fields[18].type "longstring"
+classes[1].id 109252281
+classes[1].name "summ"
+classes[1].fields[0].name "snippet"
+classes[1].fields[0].type "longstring"
+classes[1].fields[1].name "snippet2"
+classes[1].fields[1].type "longstring"
+classes[1].fields[2].name "rankfeatures"
+classes[1].fields[2].type "featuredata"
+classes[1].fields[3].name "summaryfeatures"
+classes[1].fields[3].type "featuredata" \ No newline at end of file
diff --git a/config-model/src/test/derived/twostreamingstructs/summarymap.cfg b/config-model/src/test/derived/twostreamingstructs/summarymap.cfg
new file mode 100644
index 00000000000..1bf13ffa199
--- /dev/null
+++ b/config-model/src/test/derived/twostreamingstructs/summarymap.cfg
@@ -0,0 +1,13 @@
+defaultoutputclass -1
+override[0].field "snippet"
+override[0].command "dynamicteaser"
+override[0].arguments "snippet"
+override[1].field "rankfeatures"
+override[1].command "rankfeatures"
+override[1].arguments ""
+override[2].field "summaryfeatures"
+override[2].command "summaryfeatures"
+override[2].arguments ""
+override[3].field "g"
+override[3].command "dynamicteaser"
+override[3].arguments "g" \ No newline at end of file
diff --git a/config-model/src/test/derived/twostreamingstructs/vsmfields.cfg b/config-model/src/test/derived/twostreamingstructs/vsmfields.cfg
new file mode 100644
index 00000000000..ec8b1eeb4ac
--- /dev/null
+++ b/config-model/src/test/derived/twostreamingstructs/vsmfields.cfg
@@ -0,0 +1,322 @@
+documentverificationlevel 0
+searchall 1
+fieldspec[0].name "coupleof"
+fieldspec[0].searchmethod AUTOUTF8
+fieldspec[0].arg1 ""
+fieldspec[1].name "normalfields"
+fieldspec[1].searchmethod AUTOUTF8
+fieldspec[1].arg1 ""
+fieldspec[2].name "a.f1"
+fieldspec[2].searchmethod AUTOUTF8
+fieldspec[2].arg1 ""
+fieldspec[3].name "a.f1s"
+fieldspec[3].searchmethod AUTOUTF8
+fieldspec[3].arg1 "substring"
+fieldspec[4].name "a.f2"
+fieldspec[4].searchmethod INT32
+fieldspec[4].arg1 ""
+fieldspec[5].name "a.f3"
+fieldspec[5].searchmethod DOUBLE
+fieldspec[5].arg1 ""
+fieldspec[6].name "m.key"
+fieldspec[6].searchmethod INT64
+fieldspec[6].arg1 ""
+fieldspec[7].name "m.value"
+fieldspec[7].searchmethod AUTOUTF8
+fieldspec[7].arg1 ""
+fieldspec[8].name "b.f1"
+fieldspec[8].searchmethod AUTOUTF8
+fieldspec[8].arg1 ""
+fieldspec[9].name "b.f1s"
+fieldspec[9].searchmethod AUTOUTF8
+fieldspec[9].arg1 "substring"
+fieldspec[10].name "b.f2"
+fieldspec[10].searchmethod INT32
+fieldspec[10].arg1 ""
+fieldspec[11].name "b.f3"
+fieldspec[11].searchmethod DOUBLE
+fieldspec[11].arg1 ""
+fieldspec[12].name "c.f1"
+fieldspec[12].searchmethod AUTOUTF8
+fieldspec[12].arg1 ""
+fieldspec[13].name "c.f1s"
+fieldspec[13].searchmethod AUTOUTF8
+fieldspec[13].arg1 "prefix"
+fieldspec[14].name "c.f3"
+fieldspec[14].searchmethod DOUBLE
+fieldspec[14].arg1 ""
+fieldspec[15].name "c2.f1"
+fieldspec[15].searchmethod AUTOUTF8
+fieldspec[15].arg1 ""
+fieldspec[16].name "c2.f1s"
+fieldspec[16].searchmethod AUTOUTF8
+fieldspec[16].arg1 "suffix"
+fieldspec[17].name "c2.f2"
+fieldspec[17].searchmethod INT32
+fieldspec[17].arg1 ""
+fieldspec[18].name "c2.f3"
+fieldspec[18].searchmethod DOUBLE
+fieldspec[18].arg1 ""
+fieldspec[19].name "c3.f1"
+fieldspec[19].searchmethod AUTOUTF8
+fieldspec[19].arg1 ""
+fieldspec[20].name "c3.f1s"
+fieldspec[20].searchmethod AUTOUTF8
+fieldspec[20].arg1 "prefix"
+fieldspec[21].name "c3.f2"
+fieldspec[21].searchmethod INT32
+fieldspec[21].arg1 ""
+fieldspec[22].name "c3.f3"
+fieldspec[22].searchmethod DOUBLE
+fieldspec[22].arg1 ""
+fieldspec[23].name "n.nf1.f1"
+fieldspec[23].searchmethod AUTOUTF8
+fieldspec[23].arg1 ""
+fieldspec[24].name "n.nf1.f1s"
+fieldspec[24].searchmethod AUTOUTF8
+fieldspec[24].arg1 "prefix"
+fieldspec[25].name "n.nf1.f3"
+fieldspec[25].searchmethod DOUBLE
+fieldspec[25].arg1 ""
+fieldspec[26].name "n.nf1s.f1"
+fieldspec[26].searchmethod AUTOUTF8
+fieldspec[26].arg1 "substring"
+fieldspec[27].name "n.nf1s.f1s"
+fieldspec[27].searchmethod AUTOUTF8
+fieldspec[27].arg1 "substring"
+fieldspec[28].name "n.nf1s.f2"
+fieldspec[28].searchmethod INT32
+fieldspec[28].arg1 ""
+fieldspec[29].name "n.nf1s.f3"
+fieldspec[29].searchmethod DOUBLE
+fieldspec[29].arg1 ""
+fieldspec[30].name "n.nf2"
+fieldspec[30].searchmethod AUTOUTF8
+fieldspec[30].arg1 ""
+fieldspec[31].name "array1.f1"
+fieldspec[31].searchmethod AUTOUTF8
+fieldspec[31].arg1 ""
+fieldspec[32].name "array1.f1s"
+fieldspec[32].searchmethod AUTOUTF8
+fieldspec[32].arg1 "substring"
+fieldspec[33].name "array1.f2"
+fieldspec[33].searchmethod INT32
+fieldspec[33].arg1 ""
+fieldspec[34].name "array1.f3"
+fieldspec[34].searchmethod DOUBLE
+fieldspec[34].arg1 ""
+fieldspec[35].name "array2.f1"
+fieldspec[35].searchmethod AUTOUTF8
+fieldspec[35].arg1 ""
+fieldspec[36].name "array2.f1s"
+fieldspec[36].searchmethod AUTOUTF8
+fieldspec[36].arg1 "substring"
+fieldspec[37].name "array2.f2"
+fieldspec[37].searchmethod INT32
+fieldspec[37].arg1 ""
+fieldspec[38].name "array2.f3"
+fieldspec[38].searchmethod DOUBLE
+fieldspec[38].arg1 ""
+fieldspec[39].name "array3.f1"
+fieldspec[39].searchmethod AUTOUTF8
+fieldspec[39].arg1 ""
+fieldspec[40].name "array3.f1s"
+fieldspec[40].searchmethod AUTOUTF8
+fieldspec[40].arg1 "prefix"
+fieldspec[41].name "array3.f3"
+fieldspec[41].searchmethod DOUBLE
+fieldspec[41].arg1 ""
+fieldspec[42].name "subject.f1"
+fieldspec[42].searchmethod AUTOUTF8
+fieldspec[42].arg1 ""
+fieldspec[43].name "d.f1"
+fieldspec[43].searchmethod AUTOUTF8
+fieldspec[43].arg1 "prefix"
+fieldspec[44].name "d.f1s"
+fieldspec[44].searchmethod AUTOUTF8
+fieldspec[44].arg1 "prefix"
+fieldspec[45].name "d.f2"
+fieldspec[45].searchmethod INT32
+fieldspec[45].arg1 ""
+fieldspec[46].name "d.f3"
+fieldspec[46].searchmethod DOUBLE
+fieldspec[46].arg1 ""
+fieldspec[47].name "e.f1"
+fieldspec[47].searchmethod AUTOUTF8
+fieldspec[47].arg1 "substring"
+fieldspec[48].name "e.f1s"
+fieldspec[48].searchmethod AUTOUTF8
+fieldspec[48].arg1 "substring"
+fieldspec[49].name "e.f2"
+fieldspec[49].searchmethod INT32
+fieldspec[49].arg1 ""
+fieldspec[50].name "e.f3"
+fieldspec[50].searchmethod DOUBLE
+fieldspec[50].arg1 ""
+fieldspec[51].name "f.f1"
+fieldspec[51].searchmethod AUTOUTF8
+fieldspec[51].arg1 "suffix"
+fieldspec[52].name "f.f1s"
+fieldspec[52].searchmethod AUTOUTF8
+fieldspec[52].arg1 "suffix"
+fieldspec[53].name "f.f2"
+fieldspec[53].searchmethod INT32
+fieldspec[53].arg1 ""
+fieldspec[54].name "f.f3"
+fieldspec[54].searchmethod DOUBLE
+fieldspec[54].arg1 ""
+fieldspec[55].name "g"
+fieldspec[55].searchmethod AUTOUTF8
+fieldspec[55].arg1 ""
+documenttype[0].name "streamingstruct"
+documenttype[0].index[0].name "coupleof"
+documenttype[0].index[0].field[0].name "coupleof"
+documenttype[0].index[1].name "normalfields"
+documenttype[0].index[1].field[0].name "normalfields"
+documenttype[0].index[2].name "b"
+documenttype[0].index[2].field[0].name "b.f1"
+documenttype[0].index[2].field[1].name "b.f1s"
+documenttype[0].index[2].field[2].name "b.f2"
+documenttype[0].index[2].field[3].name "b.f3"
+documenttype[0].index[3].name "b.f1"
+documenttype[0].index[3].field[0].name "b.f1"
+documenttype[0].index[4].name "b.f1s"
+documenttype[0].index[4].field[0].name "b.f1s"
+documenttype[0].index[5].name "b.f2"
+documenttype[0].index[5].field[0].name "b.f2"
+documenttype[0].index[6].name "b.f3"
+documenttype[0].index[6].field[0].name "b.f3"
+documenttype[0].index[7].name "c"
+documenttype[0].index[7].field[0].name "c.f1"
+documenttype[0].index[7].field[1].name "c.f1s"
+documenttype[0].index[7].field[2].name "c.f3"
+documenttype[0].index[8].name "c.f1"
+documenttype[0].index[8].field[0].name "c.f1"
+documenttype[0].index[9].name "c.f1s"
+documenttype[0].index[9].field[0].name "c.f1s"
+documenttype[0].index[10].name "c.f3"
+documenttype[0].index[10].field[0].name "c.f3"
+documenttype[0].index[11].name "c2"
+documenttype[0].index[11].field[0].name "c2.f1"
+documenttype[0].index[11].field[1].name "c2.f1s"
+documenttype[0].index[11].field[2].name "c2.f2"
+documenttype[0].index[11].field[3].name "c2.f3"
+documenttype[0].index[12].name "c2.f1"
+documenttype[0].index[12].field[0].name "c2.f1"
+documenttype[0].index[13].name "c2.f1s"
+documenttype[0].index[13].field[0].name "c2.f1s"
+documenttype[0].index[14].name "c2.f2"
+documenttype[0].index[14].field[0].name "c2.f2"
+documenttype[0].index[15].name "c2.f3"
+documenttype[0].index[15].field[0].name "c2.f3"
+documenttype[0].index[16].name "c3"
+documenttype[0].index[16].field[0].name "c3.f1"
+documenttype[0].index[16].field[1].name "c3.f1s"
+documenttype[0].index[16].field[2].name "c3.f2"
+documenttype[0].index[16].field[3].name "c3.f3"
+documenttype[0].index[17].name "c3.f1"
+documenttype[0].index[17].field[0].name "c3.f1"
+documenttype[0].index[18].name "c3.f1s"
+documenttype[0].index[18].field[0].name "c3.f1s"
+documenttype[0].index[19].name "c3.f2"
+documenttype[0].index[19].field[0].name "c3.f2"
+documenttype[0].index[20].name "c3.f3"
+documenttype[0].index[20].field[0].name "c3.f3"
+documenttype[0].index[21].name "n"
+documenttype[0].index[21].field[0].name "n.nf1.f1"
+documenttype[0].index[21].field[1].name "n.nf1.f1s"
+documenttype[0].index[21].field[2].name "n.nf1.f3"
+documenttype[0].index[21].field[3].name "n.nf1s.f1"
+documenttype[0].index[21].field[4].name "n.nf1s.f1s"
+documenttype[0].index[21].field[5].name "n.nf1s.f2"
+documenttype[0].index[21].field[6].name "n.nf1s.f3"
+documenttype[0].index[21].field[7].name "n.nf2"
+documenttype[0].index[22].name "n.nf1"
+documenttype[0].index[22].field[0].name "n.nf1.f1"
+documenttype[0].index[22].field[1].name "n.nf1.f1s"
+documenttype[0].index[22].field[2].name "n.nf1.f3"
+documenttype[0].index[23].name "n.nf1.f1"
+documenttype[0].index[23].field[0].name "n.nf1.f1"
+documenttype[0].index[24].name "n.nf1.f1s"
+documenttype[0].index[24].field[0].name "n.nf1.f1s"
+documenttype[0].index[25].name "n.nf1.f3"
+documenttype[0].index[25].field[0].name "n.nf1.f3"
+documenttype[0].index[26].name "n.nf1s"
+documenttype[0].index[26].field[0].name "n.nf1s.f1"
+documenttype[0].index[26].field[1].name "n.nf1s.f1s"
+documenttype[0].index[26].field[2].name "n.nf1s.f2"
+documenttype[0].index[26].field[3].name "n.nf1s.f3"
+documenttype[0].index[27].name "n.nf1s.f1"
+documenttype[0].index[27].field[0].name "n.nf1s.f1"
+documenttype[0].index[28].name "n.nf1s.f1s"
+documenttype[0].index[28].field[0].name "n.nf1s.f1s"
+documenttype[0].index[29].name "n.nf1s.f2"
+documenttype[0].index[29].field[0].name "n.nf1s.f2"
+documenttype[0].index[30].name "n.nf1s.f3"
+documenttype[0].index[30].field[0].name "n.nf1s.f3"
+documenttype[0].index[31].name "n.nf2"
+documenttype[0].index[31].field[0].name "n.nf2"
+documenttype[0].index[32].name "array2"
+documenttype[0].index[32].field[0].name "array2.f1"
+documenttype[0].index[32].field[1].name "array2.f1s"
+documenttype[0].index[32].field[2].name "array2.f2"
+documenttype[0].index[32].field[3].name "array2.f3"
+documenttype[0].index[33].name "array2.f1"
+documenttype[0].index[33].field[0].name "array2.f1"
+documenttype[0].index[34].name "array2.f1s"
+documenttype[0].index[34].field[0].name "array2.f1s"
+documenttype[0].index[35].name "array2.f2"
+documenttype[0].index[35].field[0].name "array2.f2"
+documenttype[0].index[36].name "array2.f3"
+documenttype[0].index[36].field[0].name "array2.f3"
+documenttype[0].index[37].name "array3"
+documenttype[0].index[37].field[0].name "array3.f1"
+documenttype[0].index[37].field[1].name "array3.f1s"
+documenttype[0].index[37].field[2].name "array3.f3"
+documenttype[0].index[38].name "array3.f1"
+documenttype[0].index[38].field[0].name "array3.f1"
+documenttype[0].index[39].name "array3.f1s"
+documenttype[0].index[39].field[0].name "array3.f1s"
+documenttype[0].index[40].name "array3.f3"
+documenttype[0].index[40].field[0].name "array3.f3"
+documenttype[0].index[41].name "d"
+documenttype[0].index[41].field[0].name "d.f1"
+documenttype[0].index[41].field[1].name "d.f1s"
+documenttype[0].index[41].field[2].name "d.f2"
+documenttype[0].index[41].field[3].name "d.f3"
+documenttype[0].index[42].name "d.f1"
+documenttype[0].index[42].field[0].name "d.f1"
+documenttype[0].index[43].name "d.f1s"
+documenttype[0].index[43].field[0].name "d.f1s"
+documenttype[0].index[44].name "d.f2"
+documenttype[0].index[44].field[0].name "d.f2"
+documenttype[0].index[45].name "d.f3"
+documenttype[0].index[45].field[0].name "d.f3"
+documenttype[0].index[46].name "e"
+documenttype[0].index[46].field[0].name "e.f1"
+documenttype[0].index[46].field[1].name "e.f1s"
+documenttype[0].index[46].field[2].name "e.f2"
+documenttype[0].index[46].field[3].name "e.f3"
+documenttype[0].index[47].name "e.f1"
+documenttype[0].index[47].field[0].name "e.f1"
+documenttype[0].index[48].name "e.f1s"
+documenttype[0].index[48].field[0].name "e.f1s"
+documenttype[0].index[49].name "e.f2"
+documenttype[0].index[49].field[0].name "e.f2"
+documenttype[0].index[50].name "e.f3"
+documenttype[0].index[50].field[0].name "e.f3"
+documenttype[0].index[51].name "f"
+documenttype[0].index[51].field[0].name "f.f1"
+documenttype[0].index[51].field[1].name "f.f1s"
+documenttype[0].index[51].field[2].name "f.f2"
+documenttype[0].index[51].field[3].name "f.f3"
+documenttype[0].index[52].name "f.f1"
+documenttype[0].index[52].field[0].name "f.f1"
+documenttype[0].index[53].name "f.f1s"
+documenttype[0].index[53].field[0].name "f.f1s"
+documenttype[0].index[54].name "f.f2"
+documenttype[0].index[54].field[0].name "f.f2"
+documenttype[0].index[55].name "f.f3"
+documenttype[0].index[55].field[0].name "f.f3"
+documenttype[0].index[56].name "g"
+documenttype[0].index[56].field[0].name "g" \ No newline at end of file
diff --git a/config-model/src/test/derived/twostreamingstructs/vsmsummary.cfg b/config-model/src/test/derived/twostreamingstructs/vsmsummary.cfg
new file mode 100644
index 00000000000..b7a75932f5c
--- /dev/null
+++ b/config-model/src/test/derived/twostreamingstructs/vsmsummary.cfg
@@ -0,0 +1,83 @@
+outputclass ""
+fieldmap[0].summary "coupleof"
+fieldmap[0].document[0].field "coupleof"
+fieldmap[0].command NONE
+fieldmap[1].summary "anothersummaryfield"
+fieldmap[1].document[0].field "normalfields"
+fieldmap[1].command NONE
+fieldmap[2].summary "a"
+fieldmap[2].document[0].field "a.f1"
+fieldmap[2].document[1].field "a.f1s"
+fieldmap[2].document[2].field "a.f2"
+fieldmap[2].document[3].field "a.f3"
+fieldmap[2].command NONE
+fieldmap[3].summary "m"
+fieldmap[3].document[0].field "m"
+fieldmap[3].command NONE
+fieldmap[4].summary "b"
+fieldmap[4].document[0].field "b.f1"
+fieldmap[4].document[1].field "b.f1s"
+fieldmap[4].document[2].field "b.f2"
+fieldmap[4].document[3].field "b.f3"
+fieldmap[4].command NONE
+fieldmap[5].summary "c"
+fieldmap[5].document[0].field "c.f1"
+fieldmap[5].document[1].field "c.f1s"
+fieldmap[5].document[2].field "c.f3"
+fieldmap[5].command NONE
+fieldmap[6].summary "c2"
+fieldmap[6].document[0].field "c2.f1"
+fieldmap[6].document[1].field "c2.f1s"
+fieldmap[6].document[2].field "c2.f2"
+fieldmap[6].document[3].field "c2.f3"
+fieldmap[6].command NONE
+fieldmap[7].summary "c3"
+fieldmap[7].document[0].field "c3.f1"
+fieldmap[7].document[1].field "c3.f2"
+fieldmap[7].document[2].field "c3.f3"
+fieldmap[7].command NONE
+fieldmap[8].summary "n"
+fieldmap[8].document[0].field "n.nf1.f1"
+fieldmap[8].document[1].field "n.nf1.f1s"
+fieldmap[8].document[2].field "n.nf1s.f1"
+fieldmap[8].document[3].field "n.nf1s.f1s"
+fieldmap[8].document[4].field "n.nf1s.f2"
+fieldmap[8].document[5].field "n.nf1s.f3"
+fieldmap[8].document[6].field "n.nf2"
+fieldmap[8].command NONE
+fieldmap[9].summary "array1"
+fieldmap[9].document[0].field "array1.f1"
+fieldmap[9].document[1].field "array1.f1s"
+fieldmap[9].document[2].field "array1.f2"
+fieldmap[9].document[3].field "array1.f3"
+fieldmap[9].command NONE
+fieldmap[10].summary "array2"
+fieldmap[10].document[0].field "array2.f1"
+fieldmap[10].document[1].field "array2.f1s"
+fieldmap[10].document[2].field "array2.f2"
+fieldmap[10].document[3].field "array2.f3"
+fieldmap[10].command NONE
+fieldmap[11].summary "array3"
+fieldmap[11].document[0].field "array3.f1"
+fieldmap[11].document[1].field "array3.f1s"
+fieldmap[11].document[2].field "array3.f3"
+fieldmap[11].command NONE
+fieldmap[12].summary "subject"
+fieldmap[12].document[0].field "subject.f1"
+fieldmap[12].command NONE
+fieldmap[13].summary "g"
+fieldmap[13].document[0].field "g"
+fieldmap[13].command FLATTENJUNIPER
+fieldmap[14].summary "rankfeatures"
+fieldmap[14].command NONE
+fieldmap[15].summary "summaryfeatures"
+fieldmap[15].command NONE
+fieldmap[16].summary "snippet"
+fieldmap[16].document[0].field "a.f1"
+fieldmap[16].document[1].field "b.f2"
+fieldmap[16].command FLATTENJUNIPER
+fieldmap[17].summary "snippet2"
+fieldmap[17].document[0].field "a.f1"
+fieldmap[17].document[1].field "b.f1"
+fieldmap[17].document[2].field "b.f2"
+fieldmap[17].command FLATTENSPACE \ No newline at end of file
diff --git a/config-model/src/test/derived/twostreamingstructs/whatever.sd b/config-model/src/test/derived/twostreamingstructs/whatever.sd
new file mode 100644
index 00000000000..0ddc655df15
--- /dev/null
+++ b/config-model/src/test/derived/twostreamingstructs/whatever.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search whatever {
+
+ document whatever {
+ struct pair {
+ field key type string {}
+ field value type string {}
+ }
+
+ field f1 type pair {
+ indexing: summary
+ body
+ }
+ }
+
+}
diff --git a/config-model/src/test/derived/types/attributes.cfg b/config-model/src/test/derived/types/attributes.cfg
new file mode 100644
index 00000000000..f20562d2a3c
--- /dev/null
+++ b/config-model/src/test/derived/types/attributes.cfg
@@ -0,0 +1,209 @@
+attribute[0].name "abyte"
+attribute[0].datatype INT8
+attribute[0].collectiontype SINGLE
+attribute[0].removeifzero false
+attribute[0].createifnonexistent false
+attribute[0].fastsearch false
+attribute[0].huge false
+attribute[0].sortascending true
+attribute[0].sortfunction UCA
+attribute[0].sortstrength PRIMARY
+attribute[0].sortlocale ""
+attribute[0].enablebitvectors false
+attribute[0].enableonlybitvector false
+attribute[0].fastaccess false
+attribute[0].arity 8
+attribute[0].lowerbound -9223372036854775808
+attribute[0].upperbound 9223372036854775807
+attribute[0].densepostinglistthreshold 0.4
+attribute[0].tensortype ""
+attribute[1].name "along"
+attribute[1].datatype INT64
+attribute[1].collectiontype SINGLE
+attribute[1].removeifzero false
+attribute[1].createifnonexistent false
+attribute[1].fastsearch false
+attribute[1].huge false
+attribute[1].sortascending true
+attribute[1].sortfunction UCA
+attribute[1].sortstrength PRIMARY
+attribute[1].sortlocale ""
+attribute[1].enablebitvectors false
+attribute[1].enableonlybitvector false
+attribute[1].fastaccess false
+attribute[1].arity 8
+attribute[1].lowerbound -9223372036854775808
+attribute[1].upperbound 9223372036854775807
+attribute[1].densepostinglistthreshold 0.4
+attribute[1].tensortype ""
+attribute[2].name "arrayfield"
+attribute[2].datatype INT32
+attribute[2].collectiontype ARRAY
+attribute[2].removeifzero false
+attribute[2].createifnonexistent false
+attribute[2].fastsearch false
+attribute[2].huge false
+attribute[2].sortascending true
+attribute[2].sortfunction UCA
+attribute[2].sortstrength PRIMARY
+attribute[2].sortlocale ""
+attribute[2].enablebitvectors false
+attribute[2].enableonlybitvector false
+attribute[2].fastaccess false
+attribute[2].arity 8
+attribute[2].lowerbound -9223372036854775808
+attribute[2].upperbound 9223372036854775807
+attribute[2].densepostinglistthreshold 0.4
+attribute[2].tensortype ""
+attribute[3].name "setfield"
+attribute[3].datatype STRING
+attribute[3].collectiontype WEIGHTEDSET
+attribute[3].removeifzero false
+attribute[3].createifnonexistent false
+attribute[3].fastsearch false
+attribute[3].huge false
+attribute[3].sortascending true
+attribute[3].sortfunction UCA
+attribute[3].sortstrength PRIMARY
+attribute[3].sortlocale ""
+attribute[3].enablebitvectors false
+attribute[3].enableonlybitvector false
+attribute[3].fastaccess false
+attribute[3].arity 8
+attribute[3].lowerbound -9223372036854775808
+attribute[3].upperbound 9223372036854775807
+attribute[3].densepostinglistthreshold 0.4
+attribute[3].tensortype ""
+attribute[4].name "setfield2"
+attribute[4].datatype STRING
+attribute[4].collectiontype WEIGHTEDSET
+attribute[4].removeifzero true
+attribute[4].createifnonexistent true
+attribute[4].fastsearch false
+attribute[4].huge false
+attribute[4].sortascending true
+attribute[4].sortfunction UCA
+attribute[4].sortstrength PRIMARY
+attribute[4].sortlocale ""
+attribute[4].enablebitvectors false
+attribute[4].enableonlybitvector false
+attribute[4].fastaccess false
+attribute[4].arity 8
+attribute[4].lowerbound -9223372036854775808
+attribute[4].upperbound 9223372036854775807
+attribute[4].densepostinglistthreshold 0.4
+attribute[4].tensortype ""
+attribute[5].name "setfield3"
+attribute[5].datatype STRING
+attribute[5].collectiontype WEIGHTEDSET
+attribute[5].removeifzero true
+attribute[5].createifnonexistent false
+attribute[5].fastsearch false
+attribute[5].huge false
+attribute[5].sortascending true
+attribute[5].sortfunction UCA
+attribute[5].sortstrength PRIMARY
+attribute[5].sortlocale ""
+attribute[5].enablebitvectors false
+attribute[5].enableonlybitvector false
+attribute[5].fastaccess false
+attribute[5].arity 8
+attribute[5].lowerbound -9223372036854775808
+attribute[5].upperbound 9223372036854775807
+attribute[5].densepostinglistthreshold 0.4
+attribute[5].tensortype ""
+attribute[6].name "setfield4"
+attribute[6].datatype STRING
+attribute[6].collectiontype WEIGHTEDSET
+attribute[6].removeifzero false
+attribute[6].createifnonexistent true
+attribute[6].fastsearch false
+attribute[6].huge false
+attribute[6].sortascending true
+attribute[6].sortfunction UCA
+attribute[6].sortstrength PRIMARY
+attribute[6].sortlocale ""
+attribute[6].enablebitvectors false
+attribute[6].enableonlybitvector false
+attribute[6].fastaccess false
+attribute[6].arity 8
+attribute[6].lowerbound -9223372036854775808
+attribute[6].upperbound 9223372036854775807
+attribute[6].densepostinglistthreshold 0.4
+attribute[6].tensortype ""
+attribute[7].name "tagfield"
+attribute[7].datatype STRING
+attribute[7].collectiontype WEIGHTEDSET
+attribute[7].removeifzero true
+attribute[7].createifnonexistent true
+attribute[7].fastsearch false
+attribute[7].huge false
+attribute[7].sortascending true
+attribute[7].sortfunction UCA
+attribute[7].sortstrength PRIMARY
+attribute[7].sortlocale ""
+attribute[7].enablebitvectors false
+attribute[7].enableonlybitvector false
+attribute[7].fastaccess false
+attribute[7].arity 8
+attribute[7].lowerbound -9223372036854775808
+attribute[7].upperbound 9223372036854775807
+attribute[7].densepostinglistthreshold 0.4
+attribute[7].tensortype ""
+attribute[8].name "juletre"
+attribute[8].datatype INT64
+attribute[8].collectiontype SINGLE
+attribute[8].removeifzero false
+attribute[8].createifnonexistent false
+attribute[8].fastsearch true
+attribute[8].huge false
+attribute[8].sortascending true
+attribute[8].sortfunction UCA
+attribute[8].sortstrength PRIMARY
+attribute[8].sortlocale ""
+attribute[8].enablebitvectors false
+attribute[8].enableonlybitvector false
+attribute[8].fastaccess false
+attribute[8].arity 8
+attribute[8].lowerbound -9223372036854775808
+attribute[8].upperbound 9223372036854775807
+attribute[8].densepostinglistthreshold 0.4
+attribute[8].tensortype ""
+attribute[9].name "album1"
+attribute[9].datatype STRING
+attribute[9].collectiontype WEIGHTEDSET
+attribute[9].removeifzero true
+attribute[9].createifnonexistent true
+attribute[9].fastsearch false
+attribute[9].huge false
+attribute[9].sortascending true
+attribute[9].sortfunction UCA
+attribute[9].sortstrength PRIMARY
+attribute[9].sortlocale ""
+attribute[9].enablebitvectors false
+attribute[9].enableonlybitvector false
+attribute[9].fastaccess false
+attribute[9].arity 8
+attribute[9].lowerbound -9223372036854775808
+attribute[9].upperbound 9223372036854775807
+attribute[9].densepostinglistthreshold 0.4
+attribute[9].tensortype ""
+attribute[10].name "other"
+attribute[10].datatype INT64
+attribute[10].collectiontype SINGLE
+attribute[10].removeifzero false
+attribute[10].createifnonexistent false
+attribute[10].fastsearch false
+attribute[10].huge false
+attribute[10].sortascending true
+attribute[10].sortfunction UCA
+attribute[10].sortstrength PRIMARY
+attribute[10].sortlocale ""
+attribute[10].enablebitvectors false
+attribute[10].enableonlybitvector false
+attribute[10].fastaccess false
+attribute[10].arity 8
+attribute[10].lowerbound -9223372036854775808
+attribute[10].upperbound 9223372036854775807
+attribute[10].densepostinglistthreshold 0.4
+attribute[10].tensortype ""
diff --git a/config-model/src/test/derived/types/documentmanager.cfg b/config-model/src/test/derived/types/documentmanager.cfg
new file mode 100644
index 00000000000..a2018ffa195
--- /dev/null
+++ b/config-model/src/test/derived/types/documentmanager.cfg
@@ -0,0 +1,218 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -1865479609
+datatype[1].maptype[0].keytype 2
+datatype[1].maptype[0].valtype 4
+datatype[2].id 294108848
+datatype[2].structtype[0].name "folder"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "Version"
+datatype[2].structtype[0].field[0].datatype 0
+datatype[2].structtype[0].field[1].name "Name"
+datatype[2].structtype[0].field[1].datatype 2
+datatype[2].structtype[0].field[2].name "FlagsCounter"
+datatype[2].structtype[0].field[2].datatype -1865479609
+datatype[2].structtype[0].field[3].name "anotherfolder"
+datatype[2].structtype[0].field[3].datatype 294108848
+datatype[3].id 109267174
+datatype[3].structtype[0].name "sct"
+datatype[3].structtype[0].version 0
+datatype[3].structtype[0].compresstype NONE
+datatype[3].structtype[0].compresslevel 0
+datatype[3].structtype[0].compressthreshold 95
+datatype[3].structtype[0].compressminsize 800
+datatype[3].structtype[0].field[0].name "s1"
+datatype[3].structtype[0].field[0].datatype 2
+datatype[3].structtype[0].field[1].name "s2"
+datatype[3].structtype[0].field[1].datatype 2
+datatype[4].id 49942803
+datatype[4].arraytype[0].datatype 16
+datatype[5].id 339965458
+datatype[5].maptype[0].keytype 2
+datatype[5].maptype[0].valtype 2
+datatype[6].id -2092985853
+datatype[6].structtype[0].name "mystruct"
+datatype[6].structtype[0].version 0
+datatype[6].structtype[0].compresstype NONE
+datatype[6].structtype[0].compresslevel 0
+datatype[6].structtype[0].compressthreshold 95
+datatype[6].structtype[0].compressminsize 800
+datatype[6].structtype[0].field[0].name "bytearr"
+datatype[6].structtype[0].field[0].datatype 49942803
+datatype[6].structtype[0].field[1].name "mymap"
+datatype[6].structtype[0].field[1].datatype 339965458
+datatype[6].structtype[0].field[2].name "title"
+datatype[6].structtype[0].field[2].datatype 2
+datatype[6].structtype[0].field[3].name "structfield"
+datatype[6].structtype[0].field[3].datatype 2
+datatype[7].id -1245117006
+datatype[7].arraytype[0].datatype 0
+datatype[8].id 1328286588
+datatype[8].weightedsettype[0].datatype 2
+datatype[8].weightedsettype[0].createifnonexistant false
+datatype[8].weightedsettype[0].removeifzero false
+datatype[9].id 2125328771
+datatype[9].weightedsettype[0].datatype 2
+datatype[9].weightedsettype[0].createifnonexistant false
+datatype[9].weightedsettype[0].removeifzero true
+datatype[10].id 2065577986
+datatype[10].weightedsettype[0].datatype 2
+datatype[10].weightedsettype[0].createifnonexistant true
+datatype[10].weightedsettype[0].removeifzero false
+datatype[11].id -1244829667
+datatype[11].arraytype[0].datatype 109267174
+datatype[12].id -1584287606
+datatype[12].maptype[0].keytype 2
+datatype[12].maptype[0].valtype 0
+datatype[13].id 2125154557
+datatype[13].maptype[0].keytype 2
+datatype[13].maptype[0].valtype 1
+datatype[14].id -1715531035
+datatype[14].maptype[0].keytype 0
+datatype[14].maptype[0].valtype 4
+datatype[15].id 2138385264
+datatype[15].maptype[0].keytype 0
+datatype[15].maptype[0].valtype 5
+datatype[16].id 435886609
+datatype[16].maptype[0].keytype 2
+datatype[16].maptype[0].valtype -1245117006
+datatype[17].id -1486737430
+datatype[17].arraytype[0].datatype 2
+datatype[18].id 1707615575
+datatype[18].arraytype[0].datatype -1486737430
+datatype[19].id -794985308
+datatype[19].arraytype[0].datatype 1707615575
+datatype[20].id 69621385
+datatype[20].arraytype[0].datatype 339965458
+datatype[21].id 1901258752
+datatype[21].maptype[0].keytype 0
+datatype[21].maptype[0].valtype -2092985853
+datatype[22].id 759956026
+datatype[22].arraytype[0].datatype -2092985853
+datatype[23].id -389833101
+datatype[23].maptype[0].keytype 0
+datatype[23].maptype[0].valtype 294108848
+datatype[24].id 1328581348
+datatype[24].structtype[0].name "types.header"
+datatype[24].structtype[0].version 0
+datatype[24].structtype[0].compresstype NONE
+datatype[24].structtype[0].compresslevel 0
+datatype[24].structtype[0].compressthreshold 95
+datatype[24].structtype[0].compressminsize 800
+datatype[24].structtype[0].field[0].name "abyte"
+datatype[24].structtype[0].field[0].datatype 16
+datatype[24].structtype[0].field[1].name "along"
+datatype[24].structtype[0].field[1].datatype 4
+datatype[24].structtype[0].field[2].name "arrayfield"
+datatype[24].structtype[0].field[2].datatype -1245117006
+datatype[24].structtype[0].field[3].name "setfield"
+datatype[24].structtype[0].field[3].datatype 1328286588
+datatype[24].structtype[0].field[4].name "setfield2"
+datatype[24].structtype[0].field[4].datatype 18
+datatype[24].structtype[0].field[5].name "setfield3"
+datatype[24].structtype[0].field[5].datatype 2125328771
+datatype[24].structtype[0].field[6].name "setfield4"
+datatype[24].structtype[0].field[6].datatype 2065577986
+datatype[24].structtype[0].field[7].name "tagfield"
+datatype[24].structtype[0].field[7].datatype 18
+datatype[24].structtype[0].field[8].name "structfield"
+datatype[24].structtype[0].field[8].datatype 109267174
+datatype[24].structtype[0].field[9].name "structarrayfield"
+datatype[24].structtype[0].field[9].datatype -1244829667
+datatype[24].structtype[0].field[10].name "stringmapfield"
+datatype[24].structtype[0].field[10].datatype 339965458
+datatype[24].structtype[0].field[11].name "intmapfield"
+datatype[24].structtype[0].field[11].datatype -1584287606
+datatype[24].structtype[0].field[12].name "floatmapfield"
+datatype[24].structtype[0].field[12].datatype 2125154557
+datatype[24].structtype[0].field[13].name "longmapfield"
+datatype[24].structtype[0].field[13].datatype -1715531035
+datatype[24].structtype[0].field[14].name "doublemapfield"
+datatype[24].structtype[0].field[14].datatype 2138385264
+datatype[24].structtype[0].field[15].name "arraymapfield"
+datatype[24].structtype[0].field[15].datatype 435886609
+datatype[24].structtype[0].field[16].name "arrarr"
+datatype[24].structtype[0].field[16].datatype -794985308
+datatype[24].structtype[0].field[17].name "maparr"
+datatype[24].structtype[0].field[17].datatype 69621385
+datatype[24].structtype[0].field[18].name "mystructfield"
+datatype[24].structtype[0].field[18].datatype -2092985853
+datatype[24].structtype[0].field[19].name "mystructmap"
+datatype[24].structtype[0].field[19].datatype 1901258752
+datatype[24].structtype[0].field[20].name "mystructarr"
+datatype[24].structtype[0].field[20].datatype 759956026
+datatype[24].structtype[0].field[21].name "Folders"
+datatype[24].structtype[0].field[21].datatype -389833101
+datatype[24].structtype[0].field[22].name "juletre"
+datatype[24].structtype[0].field[22].datatype 4
+datatype[24].structtype[0].field[23].name "album0"
+datatype[24].structtype[0].field[23].datatype 18
+datatype[24].structtype[0].field[24].name "album1"
+datatype[24].structtype[0].field[24].datatype 18
+datatype[24].structtype[0].field[25].name "other"
+datatype[24].structtype[0].field[25].datatype 4
+datatype[24].structtype[0].field[26].name "rankfeatures"
+datatype[24].structtype[0].field[26].datatype 2
+datatype[24].structtype[0].field[27].name "summaryfeatures"
+datatype[24].structtype[0].field[27].datatype 2
+datatype[25].id 171503364
+datatype[25].maptype[0].keytype 1707615575
+datatype[25].maptype[0].valtype 0
+datatype[26].id 1100964733
+datatype[26].arraytype[0].datatype 171503364
+datatype[27].id 348447225
+datatype[27].structtype[0].name "types.body"
+datatype[27].structtype[0].version 0
+datatype[27].structtype[0].compresstype NONE
+datatype[27].structtype[0].compresslevel 0
+datatype[27].structtype[0].compressthreshold 95
+datatype[27].structtype[0].compressminsize 800
+datatype[27].structtype[0].field[0].name "complexarray"
+datatype[27].structtype[0].field[0].datatype 1100964733
+datatype[28].id -853072901
+datatype[28].documenttype[0].name "types"
+datatype[28].documenttype[0].version 0
+datatype[28].documenttype[0].inherits[0].name "document"
+datatype[28].documenttype[0].inherits[0].version 0
+datatype[28].documenttype[0].headerstruct 1328581348
+datatype[28].documenttype[0].bodystruct 348447225
+datatype[28].documenttype[0].fieldsets{[document]}.fields[0] "Folders"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[1] "abyte"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[2] "album0"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[3] "album1"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[4] "along"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[5] "arrarr"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[6] "arrayfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[7] "arraymapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[8] "complexarray"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[9] "doublemapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[10] "floatmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[11] "intmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[12] "juletre"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[13] "longmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[14] "maparr"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[15] "mystructarr"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[16] "mystructfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[17] "mystructmap"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[18] "setfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[19] "setfield2"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[20] "setfield3"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[21] "setfield4"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[22] "stringmapfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[23] "structarrayfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[24] "structfield"
+datatype[28].documenttype[0].fieldsets{[document]}.fields[25] "tagfield"
diff --git a/config-model/src/test/derived/types/ilscripts.cfg b/config-model/src/test/derived/types/ilscripts.cfg
new file mode 100644
index 00000000000..cc692e120be
--- /dev/null
+++ b/config-model/src/test/derived/types/ilscripts.cfg
@@ -0,0 +1,55 @@
+maxtermoccurrences 100
+ilscript[0].doctype "types"
+ilscript[0].docfield[0] "abyte"
+ilscript[0].docfield[1] "along"
+ilscript[0].docfield[2] "arrayfield"
+ilscript[0].docfield[3] "setfield"
+ilscript[0].docfield[4] "setfield2"
+ilscript[0].docfield[5] "setfield3"
+ilscript[0].docfield[6] "setfield4"
+ilscript[0].docfield[7] "tagfield"
+ilscript[0].docfield[8] "structfield"
+ilscript[0].docfield[9] "structarrayfield"
+ilscript[0].docfield[10] "stringmapfield"
+ilscript[0].docfield[11] "intmapfield"
+ilscript[0].docfield[12] "floatmapfield"
+ilscript[0].docfield[13] "longmapfield"
+ilscript[0].docfield[14] "doublemapfield"
+ilscript[0].docfield[15] "arraymapfield"
+ilscript[0].docfield[16] "arrarr"
+ilscript[0].docfield[17] "maparr"
+ilscript[0].docfield[18] "mystructfield"
+ilscript[0].docfield[19] "mystructmap"
+ilscript[0].docfield[20] "mystructarr"
+ilscript[0].docfield[21] "Folders"
+ilscript[0].docfield[22] "juletre"
+ilscript[0].docfield[23] "album0"
+ilscript[0].docfield[24] "album1"
+ilscript[0].docfield[25] "complexarray"
+ilscript[0].content[0] "clear_state | guard { input along | attribute other; }"
+ilscript[0].content[1] "clear_state | guard { input abyte | summary abyte | attribute abyte; }"
+ilscript[0].content[2] "clear_state | guard { input along | summary along | attribute along; }"
+ilscript[0].content[3] "clear_state | guard { input arrayfield | attribute arrayfield; }"
+ilscript[0].content[4] "clear_state | guard { input setfield | attribute setfield; }"
+ilscript[0].content[5] "clear_state | guard { input setfield2 | attribute setfield2; }"
+ilscript[0].content[6] "clear_state | guard { input setfield3 | attribute setfield3; }"
+ilscript[0].content[7] "clear_state | guard { input setfield4 | attribute setfield4; }"
+ilscript[0].content[8] "clear_state | guard { input tagfield | attribute tagfield | summary tagfield; }"
+ilscript[0].content[9] "clear_state | guard { input juletre | attribute juletre; }"
+ilscript[0].content[10] "clear_state | guard { input album0 | summary album0; }"
+ilscript[0].content[11] "clear_state | guard { input album1 | attribute album1 | summary album1; }"
+ilscript[0].content[12] "input Folders | passthrough Folders"
+ilscript[0].content[13] "input arrarr | passthrough arrarr"
+ilscript[0].content[14] "input arraymapfield | passthrough arraymapfield"
+ilscript[0].content[15] "input complexarray | passthrough complexarray"
+ilscript[0].content[16] "input doublemapfield | passthrough doublemapfield"
+ilscript[0].content[17] "input floatmapfield | passthrough floatmapfield"
+ilscript[0].content[18] "input intmapfield | passthrough intmapfield"
+ilscript[0].content[19] "input longmapfield | passthrough longmapfield"
+ilscript[0].content[20] "input maparr | passthrough maparr"
+ilscript[0].content[21] "input mystructarr | passthrough mystructarr"
+ilscript[0].content[22] "input mystructfield | passthrough mystructfield"
+ilscript[0].content[23] "input mystructmap | passthrough mystructmap"
+ilscript[0].content[24] "input stringmapfield | passthrough stringmapfield"
+ilscript[0].content[25] "input structarrayfield | passthrough structarrayfield"
+ilscript[0].content[26] "input structfield | passthrough structfield"
diff --git a/config-model/src/test/derived/types/index-info.cfg b/config-model/src/test/derived/types/index-info.cfg
new file mode 100644
index 00000000000..4b94127c687
--- /dev/null
+++ b/config-model/src/test/derived/types/index-info.cfg
@@ -0,0 +1,427 @@
+indexinfo[0].name "types"
+indexinfo[0].command[0].indexname "sddocname"
+indexinfo[0].command[0].command "index"
+indexinfo[0].command[1].indexname "sddocname"
+indexinfo[0].command[1].command "word"
+indexinfo[0].command[2].indexname "abyte"
+indexinfo[0].command[2].command "index"
+indexinfo[0].command[3].indexname "abyte"
+indexinfo[0].command[3].command "attribute"
+indexinfo[0].command[4].indexname "abyte"
+indexinfo[0].command[4].command "numerical"
+indexinfo[0].command[5].indexname "along"
+indexinfo[0].command[5].command "index"
+indexinfo[0].command[6].indexname "along"
+indexinfo[0].command[6].command "attribute"
+indexinfo[0].command[7].indexname "along"
+indexinfo[0].command[7].command "numerical"
+indexinfo[0].command[8].indexname "arrayfield"
+indexinfo[0].command[8].command "index"
+indexinfo[0].command[9].indexname "arrayfield"
+indexinfo[0].command[9].command "multivalue"
+indexinfo[0].command[10].indexname "arrayfield"
+indexinfo[0].command[10].command "attribute"
+indexinfo[0].command[11].indexname "setfield"
+indexinfo[0].command[11].command "index"
+indexinfo[0].command[12].indexname "setfield"
+indexinfo[0].command[12].command "multivalue"
+indexinfo[0].command[13].indexname "setfield"
+indexinfo[0].command[13].command "attribute"
+indexinfo[0].command[14].indexname "setfield2"
+indexinfo[0].command[14].command "index"
+indexinfo[0].command[15].indexname "setfield2"
+indexinfo[0].command[15].command "multivalue"
+indexinfo[0].command[16].indexname "setfield2"
+indexinfo[0].command[16].command "attribute"
+indexinfo[0].command[17].indexname "setfield2"
+indexinfo[0].command[17].command "word"
+indexinfo[0].command[18].indexname "setfield3"
+indexinfo[0].command[18].command "index"
+indexinfo[0].command[19].indexname "setfield3"
+indexinfo[0].command[19].command "multivalue"
+indexinfo[0].command[20].indexname "setfield3"
+indexinfo[0].command[20].command "attribute"
+indexinfo[0].command[21].indexname "setfield4"
+indexinfo[0].command[21].command "index"
+indexinfo[0].command[22].indexname "setfield4"
+indexinfo[0].command[22].command "multivalue"
+indexinfo[0].command[23].indexname "setfield4"
+indexinfo[0].command[23].command "attribute"
+indexinfo[0].command[24].indexname "tagfield"
+indexinfo[0].command[24].command "index"
+indexinfo[0].command[25].indexname "tagfield"
+indexinfo[0].command[25].command "multivalue"
+indexinfo[0].command[26].indexname "tagfield"
+indexinfo[0].command[26].command "attribute"
+indexinfo[0].command[27].indexname "structfield.s1"
+indexinfo[0].command[27].command "index"
+indexinfo[0].command[28].indexname "structfield.s2"
+indexinfo[0].command[28].command "index"
+indexinfo[0].command[29].indexname "structfield"
+indexinfo[0].command[29].command "index"
+indexinfo[0].command[30].indexname "structarrayfield.s1"
+indexinfo[0].command[30].command "index"
+indexinfo[0].command[31].indexname "structarrayfield.s2"
+indexinfo[0].command[31].command "index"
+indexinfo[0].command[32].indexname "structarrayfield"
+indexinfo[0].command[32].command "index"
+indexinfo[0].command[33].indexname "structarrayfield"
+indexinfo[0].command[33].command "multivalue"
+indexinfo[0].command[34].indexname "stringmapfield.key"
+indexinfo[0].command[34].command "index"
+indexinfo[0].command[35].indexname "stringmapfield.key"
+indexinfo[0].command[35].command "lowercase"
+indexinfo[0].command[36].indexname "stringmapfield.key"
+indexinfo[0].command[36].command "stem:SHORTEST"
+indexinfo[0].command[37].indexname "stringmapfield.key"
+indexinfo[0].command[37].command "normalize"
+indexinfo[0].command[38].indexname "stringmapfield.value"
+indexinfo[0].command[38].command "index"
+indexinfo[0].command[39].indexname "stringmapfield.value"
+indexinfo[0].command[39].command "lowercase"
+indexinfo[0].command[40].indexname "stringmapfield.value"
+indexinfo[0].command[40].command "stem:SHORTEST"
+indexinfo[0].command[41].indexname "stringmapfield.value"
+indexinfo[0].command[41].command "normalize"
+indexinfo[0].command[42].indexname "stringmapfield"
+indexinfo[0].command[42].command "index"
+indexinfo[0].command[43].indexname "stringmapfield"
+indexinfo[0].command[43].command "lowercase"
+indexinfo[0].command[44].indexname "stringmapfield"
+indexinfo[0].command[44].command "multivalue"
+indexinfo[0].command[45].indexname "intmapfield.key"
+indexinfo[0].command[45].command "index"
+indexinfo[0].command[46].indexname "intmapfield.value"
+indexinfo[0].command[46].command "index"
+indexinfo[0].command[47].indexname "intmapfield.value"
+indexinfo[0].command[47].command "numerical"
+indexinfo[0].command[48].indexname "intmapfield"
+indexinfo[0].command[48].command "index"
+indexinfo[0].command[49].indexname "intmapfield"
+indexinfo[0].command[49].command "multivalue"
+indexinfo[0].command[50].indexname "floatmapfield.key"
+indexinfo[0].command[50].command "index"
+indexinfo[0].command[51].indexname "floatmapfield.value"
+indexinfo[0].command[51].command "index"
+indexinfo[0].command[52].indexname "floatmapfield.value"
+indexinfo[0].command[52].command "numerical"
+indexinfo[0].command[53].indexname "floatmapfield"
+indexinfo[0].command[53].command "index"
+indexinfo[0].command[54].indexname "floatmapfield"
+indexinfo[0].command[54].command "multivalue"
+indexinfo[0].command[55].indexname "longmapfield.key"
+indexinfo[0].command[55].command "index"
+indexinfo[0].command[56].indexname "longmapfield.key"
+indexinfo[0].command[56].command "numerical"
+indexinfo[0].command[57].indexname "longmapfield.value"
+indexinfo[0].command[57].command "index"
+indexinfo[0].command[58].indexname "longmapfield.value"
+indexinfo[0].command[58].command "numerical"
+indexinfo[0].command[59].indexname "longmapfield"
+indexinfo[0].command[59].command "index"
+indexinfo[0].command[60].indexname "longmapfield"
+indexinfo[0].command[60].command "multivalue"
+indexinfo[0].command[61].indexname "doublemapfield.key"
+indexinfo[0].command[61].command "index"
+indexinfo[0].command[62].indexname "doublemapfield.key"
+indexinfo[0].command[62].command "numerical"
+indexinfo[0].command[63].indexname "doublemapfield.value"
+indexinfo[0].command[63].command "index"
+indexinfo[0].command[64].indexname "doublemapfield.value"
+indexinfo[0].command[64].command "numerical"
+indexinfo[0].command[65].indexname "doublemapfield"
+indexinfo[0].command[65].command "index"
+indexinfo[0].command[66].indexname "doublemapfield"
+indexinfo[0].command[66].command "multivalue"
+indexinfo[0].command[67].indexname "arraymapfield.key"
+indexinfo[0].command[67].command "index"
+indexinfo[0].command[68].indexname "arraymapfield.value"
+indexinfo[0].command[68].command "index"
+indexinfo[0].command[69].indexname "arraymapfield.value"
+indexinfo[0].command[69].command "multivalue"
+indexinfo[0].command[70].indexname "arraymapfield"
+indexinfo[0].command[70].command "index"
+indexinfo[0].command[71].indexname "arraymapfield"
+indexinfo[0].command[71].command "multivalue"
+indexinfo[0].command[72].indexname "arrarr"
+indexinfo[0].command[72].command "index"
+indexinfo[0].command[73].indexname "arrarr"
+indexinfo[0].command[73].command "multivalue"
+indexinfo[0].command[74].indexname "maparr"
+indexinfo[0].command[74].command "index"
+indexinfo[0].command[75].indexname "maparr"
+indexinfo[0].command[75].command "multivalue"
+indexinfo[0].command[76].indexname "mystructfield.bytearr"
+indexinfo[0].command[76].command "index"
+indexinfo[0].command[77].indexname "mystructfield.bytearr"
+indexinfo[0].command[77].command "multivalue"
+indexinfo[0].command[78].indexname "mystructfield.mymap.key"
+indexinfo[0].command[78].command "index"
+indexinfo[0].command[79].indexname "mystructfield.mymap.value"
+indexinfo[0].command[79].command "index"
+indexinfo[0].command[80].indexname "mystructfield.mymap"
+indexinfo[0].command[80].command "index"
+indexinfo[0].command[81].indexname "mystructfield.mymap"
+indexinfo[0].command[81].command "multivalue"
+indexinfo[0].command[82].indexname "mystructfield.title"
+indexinfo[0].command[82].command "index"
+indexinfo[0].command[83].indexname "mystructfield.structfield"
+indexinfo[0].command[83].command "index"
+indexinfo[0].command[84].indexname "mystructfield"
+indexinfo[0].command[84].command "index"
+indexinfo[0].command[85].indexname "mystructmap.key"
+indexinfo[0].command[85].command "index"
+indexinfo[0].command[86].indexname "mystructmap.key"
+indexinfo[0].command[86].command "numerical"
+indexinfo[0].command[87].indexname "mystructmap.value.bytearr"
+indexinfo[0].command[87].command "index"
+indexinfo[0].command[88].indexname "mystructmap.value.bytearr"
+indexinfo[0].command[88].command "multivalue"
+indexinfo[0].command[89].indexname "mystructmap.value.mymap.key"
+indexinfo[0].command[89].command "index"
+indexinfo[0].command[90].indexname "mystructmap.value.mymap.value"
+indexinfo[0].command[90].command "index"
+indexinfo[0].command[91].indexname "mystructmap.value.mymap"
+indexinfo[0].command[91].command "index"
+indexinfo[0].command[92].indexname "mystructmap.value.mymap"
+indexinfo[0].command[92].command "multivalue"
+indexinfo[0].command[93].indexname "mystructmap.value.title"
+indexinfo[0].command[93].command "index"
+indexinfo[0].command[94].indexname "mystructmap.value.structfield"
+indexinfo[0].command[94].command "index"
+indexinfo[0].command[95].indexname "mystructmap.value"
+indexinfo[0].command[95].command "index"
+indexinfo[0].command[96].indexname "mystructmap"
+indexinfo[0].command[96].command "index"
+indexinfo[0].command[97].indexname "mystructmap"
+indexinfo[0].command[97].command "multivalue"
+indexinfo[0].command[98].indexname "mystructarr.bytearr"
+indexinfo[0].command[98].command "index"
+indexinfo[0].command[99].indexname "mystructarr.bytearr"
+indexinfo[0].command[99].command "multivalue"
+indexinfo[0].command[100].indexname "mystructarr.mymap.key"
+indexinfo[0].command[100].command "index"
+indexinfo[0].command[101].indexname "mystructarr.mymap.value"
+indexinfo[0].command[101].command "index"
+indexinfo[0].command[102].indexname "mystructarr.mymap"
+indexinfo[0].command[102].command "index"
+indexinfo[0].command[103].indexname "mystructarr.mymap"
+indexinfo[0].command[103].command "multivalue"
+indexinfo[0].command[104].indexname "mystructarr.title"
+indexinfo[0].command[104].command "index"
+indexinfo[0].command[105].indexname "mystructarr.structfield"
+indexinfo[0].command[105].command "index"
+indexinfo[0].command[106].indexname "mystructarr"
+indexinfo[0].command[106].command "index"
+indexinfo[0].command[107].indexname "mystructarr"
+indexinfo[0].command[107].command "multivalue"
+indexinfo[0].command[108].indexname "Folders.key"
+indexinfo[0].command[108].command "index"
+indexinfo[0].command[109].indexname "Folders.key"
+indexinfo[0].command[109].command "numerical"
+indexinfo[0].command[110].indexname "Folders.value.Version"
+indexinfo[0].command[110].command "index"
+indexinfo[0].command[111].indexname "Folders.value.Version"
+indexinfo[0].command[111].command "numerical"
+indexinfo[0].command[112].indexname "Folders.value.Name"
+indexinfo[0].command[112].command "index"
+indexinfo[0].command[113].indexname "Folders.value.FlagsCounter.key"
+indexinfo[0].command[113].command "index"
+indexinfo[0].command[114].indexname "Folders.value.FlagsCounter.value"
+indexinfo[0].command[114].command "index"
+indexinfo[0].command[115].indexname "Folders.value.FlagsCounter.value"
+indexinfo[0].command[115].command "numerical"
+indexinfo[0].command[116].indexname "Folders.value.FlagsCounter"
+indexinfo[0].command[116].command "index"
+indexinfo[0].command[117].indexname "Folders.value.FlagsCounter"
+indexinfo[0].command[117].command "multivalue"
+indexinfo[0].command[118].indexname "Folders.value.anotherfolder.Version"
+indexinfo[0].command[118].command "index"
+indexinfo[0].command[119].indexname "Folders.value.anotherfolder.Version"
+indexinfo[0].command[119].command "numerical"
+indexinfo[0].command[120].indexname "Folders.value.anotherfolder.Name"
+indexinfo[0].command[120].command "index"
+indexinfo[0].command[121].indexname "Folders.value.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[121].command "index"
+indexinfo[0].command[122].indexname "Folders.value.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[122].command "index"
+indexinfo[0].command[123].indexname "Folders.value.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[123].command "numerical"
+indexinfo[0].command[124].indexname "Folders.value.anotherfolder.FlagsCounter"
+indexinfo[0].command[124].command "index"
+indexinfo[0].command[125].indexname "Folders.value.anotherfolder.FlagsCounter"
+indexinfo[0].command[125].command "multivalue"
+indexinfo[0].command[126].indexname "Folders.value.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[126].command "index"
+indexinfo[0].command[127].indexname "Folders.value.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[127].command "numerical"
+indexinfo[0].command[128].indexname "Folders.value.anotherfolder.anotherfolder.Name"
+indexinfo[0].command[128].command "index"
+indexinfo[0].command[129].indexname "Folders.value.anotherfolder.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[129].command "index"
+indexinfo[0].command[130].indexname "Folders.value.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[130].command "index"
+indexinfo[0].command[131].indexname "Folders.value.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[131].command "numerical"
+indexinfo[0].command[132].indexname "Folders.value.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[132].command "index"
+indexinfo[0].command[133].indexname "Folders.value.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[133].command "multivalue"
+indexinfo[0].command[134].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[134].command "index"
+indexinfo[0].command[135].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[135].command "numerical"
+indexinfo[0].command[136].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.Name"
+indexinfo[0].command[136].command "index"
+indexinfo[0].command[137].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[137].command "index"
+indexinfo[0].command[138].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[138].command "index"
+indexinfo[0].command[139].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[139].command "numerical"
+indexinfo[0].command[140].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[140].command "index"
+indexinfo[0].command[141].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[141].command "multivalue"
+indexinfo[0].command[142].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[142].command "index"
+indexinfo[0].command[143].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[143].command "numerical"
+indexinfo[0].command[144].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Name"
+indexinfo[0].command[144].command "index"
+indexinfo[0].command[145].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[145].command "index"
+indexinfo[0].command[146].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[146].command "index"
+indexinfo[0].command[147].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[147].command "numerical"
+indexinfo[0].command[148].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[148].command "index"
+indexinfo[0].command[149].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[149].command "multivalue"
+indexinfo[0].command[150].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[150].command "index"
+indexinfo[0].command[151].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[151].command "numerical"
+indexinfo[0].command[152].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Name"
+indexinfo[0].command[152].command "index"
+indexinfo[0].command[153].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[153].command "index"
+indexinfo[0].command[154].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[154].command "index"
+indexinfo[0].command[155].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[155].command "numerical"
+indexinfo[0].command[156].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[156].command "index"
+indexinfo[0].command[157].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[157].command "multivalue"
+indexinfo[0].command[158].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[158].command "index"
+indexinfo[0].command[159].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[159].command "numerical"
+indexinfo[0].command[160].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Name"
+indexinfo[0].command[160].command "index"
+indexinfo[0].command[161].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[161].command "index"
+indexinfo[0].command[162].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[162].command "index"
+indexinfo[0].command[163].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[163].command "numerical"
+indexinfo[0].command[164].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[164].command "index"
+indexinfo[0].command[165].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[165].command "multivalue"
+indexinfo[0].command[166].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[166].command "index"
+indexinfo[0].command[167].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[167].command "numerical"
+indexinfo[0].command[168].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Name"
+indexinfo[0].command[168].command "index"
+indexinfo[0].command[169].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[169].command "index"
+indexinfo[0].command[170].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[170].command "index"
+indexinfo[0].command[171].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[171].command "numerical"
+indexinfo[0].command[172].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[172].command "index"
+indexinfo[0].command[173].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[173].command "multivalue"
+indexinfo[0].command[174].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[174].command "index"
+indexinfo[0].command[175].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Version"
+indexinfo[0].command[175].command "numerical"
+indexinfo[0].command[176].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.Name"
+indexinfo[0].command[176].command "index"
+indexinfo[0].command[177].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.key"
+indexinfo[0].command[177].command "index"
+indexinfo[0].command[178].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[178].command "index"
+indexinfo[0].command[179].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter.value"
+indexinfo[0].command[179].command "numerical"
+indexinfo[0].command[180].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[180].command "index"
+indexinfo[0].command[181].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.FlagsCounter"
+indexinfo[0].command[181].command "multivalue"
+indexinfo[0].command[182].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder"
+indexinfo[0].command[182].command "index"
+indexinfo[0].command[183].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder"
+indexinfo[0].command[183].command "index"
+indexinfo[0].command[184].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder"
+indexinfo[0].command[184].command "index"
+indexinfo[0].command[185].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder"
+indexinfo[0].command[185].command "index"
+indexinfo[0].command[186].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder.anotherfolder"
+indexinfo[0].command[186].command "index"
+indexinfo[0].command[187].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder.anotherfolder"
+indexinfo[0].command[187].command "index"
+indexinfo[0].command[188].indexname "Folders.value.anotherfolder.anotherfolder.anotherfolder"
+indexinfo[0].command[188].command "index"
+indexinfo[0].command[189].indexname "Folders.value.anotherfolder.anotherfolder"
+indexinfo[0].command[189].command "index"
+indexinfo[0].command[190].indexname "Folders.value.anotherfolder"
+indexinfo[0].command[190].command "index"
+indexinfo[0].command[191].indexname "Folders.value"
+indexinfo[0].command[191].command "index"
+indexinfo[0].command[192].indexname "Folders"
+indexinfo[0].command[192].command "index"
+indexinfo[0].command[193].indexname "Folders"
+indexinfo[0].command[193].command "multivalue"
+indexinfo[0].command[194].indexname "juletre"
+indexinfo[0].command[194].command "index"
+indexinfo[0].command[195].indexname "juletre"
+indexinfo[0].command[195].command "attribute"
+indexinfo[0].command[196].indexname "juletre"
+indexinfo[0].command[196].command "fast-search"
+indexinfo[0].command[197].indexname "juletre"
+indexinfo[0].command[197].command "numerical"
+indexinfo[0].command[198].indexname "album0"
+indexinfo[0].command[198].command "index"
+indexinfo[0].command[199].indexname "album0"
+indexinfo[0].command[199].command "multivalue"
+indexinfo[0].command[200].indexname "album1"
+indexinfo[0].command[200].command "index"
+indexinfo[0].command[201].indexname "album1"
+indexinfo[0].command[201].command "multivalue"
+indexinfo[0].command[202].indexname "album1"
+indexinfo[0].command[202].command "attribute"
+indexinfo[0].command[203].indexname "album1"
+indexinfo[0].command[203].command "word"
+indexinfo[0].command[204].indexname "complexarray"
+indexinfo[0].command[204].command "index"
+indexinfo[0].command[205].indexname "complexarray"
+indexinfo[0].command[205].command "multivalue"
+indexinfo[0].command[206].indexname "other"
+indexinfo[0].command[206].command "index"
+indexinfo[0].command[207].indexname "other"
+indexinfo[0].command[207].command "attribute"
+indexinfo[0].command[208].indexname "other"
+indexinfo[0].command[208].command "numerical"
+indexinfo[0].command[209].indexname "pst_sta_boldingoff_nomatch_tag_01"
+indexinfo[0].command[209].command "index"
+indexinfo[0].command[210].indexname "pst_sta_boldingoff_nomatch_tag_01"
+indexinfo[0].command[210].command "multivalue"
+indexinfo[0].command[211].indexname "rankfeatures"
+indexinfo[0].command[211].command "index"
+indexinfo[0].command[212].indexname "summaryfeatures"
+indexinfo[0].command[212].command "index"
diff --git a/config-model/src/test/derived/types/rank-profiles.cfg b/config-model/src/test/derived/types/rank-profiles.cfg
new file mode 100644
index 00000000000..342a3ca0a2e
--- /dev/null
+++ b/config-model/src/test/derived/types/rank-profiles.cfg
@@ -0,0 +1,16 @@
+rankprofile[0].name "default"
+rankprofile[0].fef.property[0].name "nativeAttributeMatch.weightTable.setfield2"
+rankprofile[0].fef.property[0].value "linear(0,0)"
+rankprofile[0].fef.property[1].name "nativeAttributeMatch.weightTable.setfield3"
+rankprofile[0].fef.property[1].value "linear(0,0)"
+rankprofile[0].fef.property[2].name "nativeAttributeMatch.weightTable.tagfield"
+rankprofile[0].fef.property[2].value "loggrowth(38,50,1)"
+rankprofile[1].name "unranked"
+rankprofile[1].fef.property[0].name "vespa.rank.firstphase"
+rankprofile[1].fef.property[0].value "value(0)"
+rankprofile[1].fef.property[1].name "vespa.hitcollector.heapsize"
+rankprofile[1].fef.property[1].value "0"
+rankprofile[1].fef.property[2].name "vespa.hitcollector.arraysize"
+rankprofile[1].fef.property[2].value "0"
+rankprofile[1].fef.property[3].name "vespa.dump.ignoredefaultfeatures"
+rankprofile[1].fef.property[3].value "true" \ No newline at end of file
diff --git a/config-model/src/test/derived/types/summary.0.cfg.part.types1125507321 b/config-model/src/test/derived/types/summary.0.cfg.part.types1125507321
new file mode 100644
index 00000000000..a0a6ea62b39
--- /dev/null
+++ b/config-model/src/test/derived/types/summary.0.cfg.part.types1125507321
@@ -0,0 +1,9 @@
+classes[types1125507321].id 1125507321
+classes[types1125507321].name types
+classes[types1125507321].fields[3]
+classes[types1125507321].fields[0].name sddocname
+classes[types1125507321].fields[0].type string
+classes[types1125507321].fields[1].name abyte
+classes[types1125507321].fields[1].type byte
+classes[types1125507321].fields[2].name ranklog
+classes[types1125507321].fields[2].type string
diff --git a/config-model/src/test/derived/types/summary.cfg b/config-model/src/test/derived/types/summary.cfg
new file mode 100644
index 00000000000..6a62dedb851
--- /dev/null
+++ b/config-model/src/test/derived/types/summary.cfg
@@ -0,0 +1,35 @@
+defaultsummaryid 1103008471
+classes[0].id 1103008471
+classes[0].name "default"
+classes[0].fields[0].name "abyte"
+classes[0].fields[0].type "byte"
+classes[0].fields[1].name "along"
+classes[0].fields[1].type "int64"
+classes[0].fields[2].name "tagfield"
+classes[0].fields[2].type "jsonstring"
+classes[0].fields[3].name "stringmapfield"
+classes[0].fields[3].type "jsonstring"
+classes[0].fields[4].name "album0"
+classes[0].fields[4].type "jsonstring"
+classes[0].fields[5].name "album1"
+classes[0].fields[5].type "jsonstring"
+classes[0].fields[6].name "rankfeatures"
+classes[0].fields[6].type "featuredata"
+classes[0].fields[7].name "summaryfeatures"
+classes[0].fields[7].type "featuredata"
+classes[0].fields[8].name "documentid"
+classes[0].fields[8].type "longstring"
+classes[1].id 278794929
+classes[1].name "attributeprefetch"
+classes[1].fields[0].name "other"
+classes[1].fields[0].type "int64"
+classes[1].fields[1].name "abyte"
+classes[1].fields[1].type "byte"
+classes[1].fields[2].name "along"
+classes[1].fields[2].type "int64"
+classes[1].fields[3].name "juletre"
+classes[1].fields[3].type "int64"
+classes[1].fields[4].name "rankfeatures"
+classes[1].fields[4].type "featuredata"
+classes[1].fields[5].name "summaryfeatures"
+classes[1].fields[5].type "featuredata"
diff --git a/config-model/src/test/derived/types/summarymap.cfg b/config-model/src/test/derived/types/summarymap.cfg
new file mode 100644
index 00000000000..eac532d966b
--- /dev/null
+++ b/config-model/src/test/derived/types/summarymap.cfg
@@ -0,0 +1,25 @@
+defaultoutputclass -1
+override[0].field "abyte"
+override[0].command "attribute"
+override[0].arguments "abyte"
+override[1].field "along"
+override[1].command "attribute"
+override[1].arguments "along"
+override[2].field "tagfield"
+override[2].command "attribute"
+override[2].arguments "tagfield"
+override[3].field "album1"
+override[3].command "attribute"
+override[3].arguments "album1"
+override[4].field "rankfeatures"
+override[4].command "rankfeatures"
+override[4].arguments ""
+override[5].field "summaryfeatures"
+override[5].command "summaryfeatures"
+override[5].arguments ""
+override[6].field "other"
+override[6].command "attribute"
+override[6].arguments "other"
+override[7].field "juletre"
+override[7].command "attribute"
+override[7].arguments "juletre"
diff --git a/config-model/src/test/derived/types/types.sd b/config-model/src/test/derived/types/types.sd
new file mode 100644
index 00000000000..69101934461
--- /dev/null
+++ b/config-model/src/test/derived/types/types.sd
@@ -0,0 +1,150 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search types {
+
+ document types {
+
+ field abyte type byte {
+ indexing: index | summary | attribute
+ }
+
+ field along type long {
+ indexing: index | summary | attribute
+ }
+
+ field arrayfield type array<int> {
+ indexing: attribute
+ }
+
+ field setfield type weightedset<string> {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field setfield2 type weightedset<string> {
+ indexing: attribute
+ weightedset: remove-if-zero
+ weightedset: create-if-nonexistent
+ rank-type: empty
+ }
+
+ field setfield3 type weightedset<string> {
+ weightedset: remove-if-zero
+ indexing: attribute
+ rank-type: empty
+ match {
+ token
+ }
+ }
+
+ field setfield4 type weightedset<string> {
+ weightedset: create-if-nonexistent
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field tagfield type tag {
+ indexing: attribute | summary
+ match {
+ token
+ }
+ }
+ struct sct {
+ field s1 type string {}
+ field s2 type string {}
+ }
+ field structfield type sct {
+ }
+ field structarrayfield type array<sct> {
+ }
+ field stringmapfield type map<string, string> {
+ indexing: index | summary
+ }
+ field intmapfield type map<string, int> {
+
+ }
+ field floatmapfield type map<string, float> {
+
+ }
+ field longmapfield type map<int, long> {
+
+ }
+ field doublemapfield type map<int, double> {
+
+ }
+ field arraymapfield type map<string,array<int>> {
+
+ }
+ #field complexfield type map<array<sct>, map<int,array<float>>> {
+ #}
+ #field wildcardfield type map<int,?> {
+ #}
+ #field wildcardfield2 type map<?,?> {
+ #}
+
+ field arrarr type array<array<array<string>>> {header}
+ field maparr type array<map<string, string>> {header}
+ field complexarray type array< map<array<array<string>>, int> > {body}
+
+ struct mystruct {
+ field bytearr type array<byte>{}
+ field mymap type map<string, string>{}
+ field title type string {}
+ field structfield type string {}
+ }
+
+ field mystructfield type mystruct {header}
+ field mystructmap type map<int, mystruct> {header}
+ field mystructarr type array<mystruct> {header}
+
+ struct folder {
+ field Version type int {}
+ field Name type string {}
+ field FlagsCounter type map<string,long> {}
+ field anotherfolder type folder {}
+ }
+
+
+ field Folders type map<int,folder> {}
+
+ field juletre type long {
+ indexing: attribute
+ attribute {
+ fast-search
+ }
+ }
+
+ # Field defined same way as tag
+ field album0 type weightedset<string> {
+ indexing: summary
+ # This is pointless, but
+ weightedset {
+ create-if-nonexistent
+ remove-if-zero
+ }
+ header
+ }
+
+ # Field defined same way as tag
+ field album1 type weightedset<string> {
+ indexing: attribute | summary
+ weightedset {
+ create-if-nonexistent
+ remove-if-zero
+ }
+ header
+ }
+
+ }
+
+ field pst_sta_boldingoff_nomatch_tag_01 type tag {
+ body
+ }
+
+ field other type long {
+ indexing: input along | attribute
+ }
+}
diff --git a/config-model/src/test/derived/types/vsmsummary.cfg b/config-model/src/test/derived/types/vsmsummary.cfg
new file mode 100644
index 00000000000..6c641d9935e
--- /dev/null
+++ b/config-model/src/test/derived/types/vsmsummary.cfg
@@ -0,0 +1,23 @@
+outputclass ""
+fieldmap[0].summary "abyte"
+fieldmap[0].document[0].field "abyte"
+fieldmap[0].command NONE
+fieldmap[1].summary "along"
+fieldmap[1].document[0].field "along"
+fieldmap[1].command NONE
+fieldmap[2].summary "tagfield"
+fieldmap[2].document[0].field "tagfield"
+fieldmap[2].command NONE
+fieldmap[3].summary "stringmapfield"
+fieldmap[3].document[0].field "stringmapfield"
+fieldmap[3].command NONE
+fieldmap[4].summary "album0"
+fieldmap[4].document[0].field "album0"
+fieldmap[4].command NONE
+fieldmap[5].summary "album1"
+fieldmap[5].document[0].field "album1"
+fieldmap[5].command NONE
+fieldmap[6].summary "rankfeatures"
+fieldmap[6].command NONE
+fieldmap[7].summary "summaryfeatures"
+fieldmap[7].command NONE \ No newline at end of file
diff --git a/config-model/src/test/derived/uri_array/ilscripts.cfg b/config-model/src/test/derived/uri_array/ilscripts.cfg
new file mode 100644
index 00000000000..de78b44199c
--- /dev/null
+++ b/config-model/src/test/derived/uri_array/ilscripts.cfg
@@ -0,0 +1,4 @@
+maxtermoccurrences 100
+ilscript[0].doctype "uri_array"
+ilscript[0].docfield[0] "my_uri"
+ilscript[0].content[0] "clear_state | guard { input my_uri | index my_uri; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/uri_array/indexschema.cfg b/config-model/src/test/derived/uri_array/indexschema.cfg
new file mode 100644
index 00000000000..4e6875bffb8
--- /dev/null
+++ b/config-model/src/test/derived/uri_array/indexschema.cfg
@@ -0,0 +1,64 @@
+indexfield[0].name "my_uri"
+indexfield[0].indextype VESPA
+indexfield[0].datatype STRING
+indexfield[0].collectiontype ARRAY
+indexfield[0].prefix false
+indexfield[0].phrases false
+indexfield[0].positions true
+indexfield[0].averageelementlen 512
+indexfield[1].name "my_uri.fragment"
+indexfield[1].indextype VESPA
+indexfield[1].datatype STRING
+indexfield[1].collectiontype ARRAY
+indexfield[1].prefix false
+indexfield[1].phrases false
+indexfield[1].positions true
+indexfield[1].averageelementlen 512
+indexfield[2].name "my_uri.host"
+indexfield[2].indextype VESPA
+indexfield[2].datatype STRING
+indexfield[2].collectiontype ARRAY
+indexfield[2].prefix false
+indexfield[2].phrases false
+indexfield[2].positions true
+indexfield[2].averageelementlen 512
+indexfield[3].name "my_uri.hostname"
+indexfield[3].indextype VESPA
+indexfield[3].datatype STRING
+indexfield[3].collectiontype ARRAY
+indexfield[3].prefix false
+indexfield[3].phrases false
+indexfield[3].positions true
+indexfield[3].averageelementlen 512
+indexfield[4].name "my_uri.path"
+indexfield[4].indextype VESPA
+indexfield[4].datatype STRING
+indexfield[4].collectiontype ARRAY
+indexfield[4].prefix false
+indexfield[4].phrases false
+indexfield[4].positions true
+indexfield[4].averageelementlen 512
+indexfield[5].name "my_uri.port"
+indexfield[5].indextype VESPA
+indexfield[5].datatype STRING
+indexfield[5].collectiontype ARRAY
+indexfield[5].prefix false
+indexfield[5].phrases false
+indexfield[5].positions true
+indexfield[5].averageelementlen 512
+indexfield[6].name "my_uri.query"
+indexfield[6].indextype VESPA
+indexfield[6].datatype STRING
+indexfield[6].collectiontype ARRAY
+indexfield[6].prefix false
+indexfield[6].phrases false
+indexfield[6].positions true
+indexfield[6].averageelementlen 512
+indexfield[7].name "my_uri.scheme"
+indexfield[7].indextype VESPA
+indexfield[7].datatype STRING
+indexfield[7].collectiontype ARRAY
+indexfield[7].prefix false
+indexfield[7].phrases false
+indexfield[7].positions true
+indexfield[7].averageelementlen 512 \ No newline at end of file
diff --git a/config-model/src/test/derived/uri_array/uri_array.sd b/config-model/src/test/derived/uri_array/uri_array.sd
new file mode 100644
index 00000000000..d71ee231238
--- /dev/null
+++ b/config-model/src/test/derived/uri_array/uri_array.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search uri_array {
+ document uri_array {
+ field my_uri type array<uri> {
+ indexing: index
+ }
+ }
+}
diff --git a/config-model/src/test/derived/uri_wset/ilscripts.cfg b/config-model/src/test/derived/uri_wset/ilscripts.cfg
new file mode 100644
index 00000000000..81066cd130d
--- /dev/null
+++ b/config-model/src/test/derived/uri_wset/ilscripts.cfg
@@ -0,0 +1,4 @@
+maxtermoccurrences 100
+ilscript[0].doctype "uri_wset"
+ilscript[0].docfield[0] "my_uri"
+ilscript[0].content[0] "clear_state | guard { input my_uri | index my_uri; }" \ No newline at end of file
diff --git a/config-model/src/test/derived/uri_wset/indexschema.cfg b/config-model/src/test/derived/uri_wset/indexschema.cfg
new file mode 100644
index 00000000000..0a750704933
--- /dev/null
+++ b/config-model/src/test/derived/uri_wset/indexschema.cfg
@@ -0,0 +1,64 @@
+indexfield[0].name "my_uri"
+indexfield[0].indextype VESPA
+indexfield[0].datatype STRING
+indexfield[0].collectiontype WEIGHTEDSET
+indexfield[0].prefix false
+indexfield[0].phrases false
+indexfield[0].positions true
+indexfield[0].averageelementlen 512
+indexfield[1].name "my_uri.fragment"
+indexfield[1].indextype VESPA
+indexfield[1].datatype STRING
+indexfield[1].collectiontype WEIGHTEDSET
+indexfield[1].prefix false
+indexfield[1].phrases false
+indexfield[1].positions true
+indexfield[1].averageelementlen 512
+indexfield[2].name "my_uri.host"
+indexfield[2].indextype VESPA
+indexfield[2].datatype STRING
+indexfield[2].collectiontype WEIGHTEDSET
+indexfield[2].prefix false
+indexfield[2].phrases false
+indexfield[2].positions true
+indexfield[2].averageelementlen 512
+indexfield[3].name "my_uri.hostname"
+indexfield[3].indextype VESPA
+indexfield[3].datatype STRING
+indexfield[3].collectiontype WEIGHTEDSET
+indexfield[3].prefix false
+indexfield[3].phrases false
+indexfield[3].positions true
+indexfield[3].averageelementlen 512
+indexfield[4].name "my_uri.path"
+indexfield[4].indextype VESPA
+indexfield[4].datatype STRING
+indexfield[4].collectiontype WEIGHTEDSET
+indexfield[4].prefix false
+indexfield[4].phrases false
+indexfield[4].positions true
+indexfield[4].averageelementlen 512
+indexfield[5].name "my_uri.port"
+indexfield[5].indextype VESPA
+indexfield[5].datatype STRING
+indexfield[5].collectiontype WEIGHTEDSET
+indexfield[5].prefix false
+indexfield[5].phrases false
+indexfield[5].positions true
+indexfield[5].averageelementlen 512
+indexfield[6].name "my_uri.query"
+indexfield[6].indextype VESPA
+indexfield[6].datatype STRING
+indexfield[6].collectiontype WEIGHTEDSET
+indexfield[6].prefix false
+indexfield[6].phrases false
+indexfield[6].positions true
+indexfield[6].averageelementlen 512
+indexfield[7].name "my_uri.scheme"
+indexfield[7].indextype VESPA
+indexfield[7].datatype STRING
+indexfield[7].collectiontype WEIGHTEDSET
+indexfield[7].prefix false
+indexfield[7].phrases false
+indexfield[7].positions true
+indexfield[7].averageelementlen 512 \ No newline at end of file
diff --git a/config-model/src/test/derived/uri_wset/uri_wset.sd b/config-model/src/test/derived/uri_wset/uri_wset.sd
new file mode 100644
index 00000000000..547ec7dc728
--- /dev/null
+++ b/config-model/src/test/derived/uri_wset/uri_wset.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search uri_wset {
+ document uri_wset {
+ field my_uri type weightedset<uri> {
+ indexing: index
+ }
+ }
+}
diff --git a/config-model/src/test/examples/arrays.sd b/config-model/src/test/examples/arrays.sd
new file mode 100644
index 00000000000..72c91bdffcd
--- /dev/null
+++ b/config-model/src/test/examples/arrays.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document testarrays {
+
+ field tags type string[] {
+ }
+
+ field ratings type int[] {
+ }
+
+}
diff --git a/config-model/src/test/examples/arraysweightedsets.sd b/config-model/src/test/examples/arraysweightedsets.sd
new file mode 100644
index 00000000000..107409ca143
--- /dev/null
+++ b/config-model/src/test/examples/arraysweightedsets.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+document testarraysweightedSets {
+
+ field tags type array<string> {
+ }
+
+ field ratings type array < int> {
+ }
+
+ field flags type weightedset < string > {
+ }
+
+ field banners type weightedset<int> {
+ }
+}
diff --git a/config-model/src/test/examples/attributeindex.sd b/config-model/src/test/examples/attributeindex.sd
new file mode 100644
index 00000000000..dc5f3fcbc25
--- /dev/null
+++ b/config-model/src/test/examples/attributeindex.sd
@@ -0,0 +1,24 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search attributeindex {
+ document attributeindex {
+
+ field nosettings type string {
+ indexing: summary | attribute | index
+ }
+
+ # Attribute and index have different names
+ field specifyname type string {
+ indexing: summary | attribute newname | index
+ }
+
+ # # index-to: with same name as attribute
+ field specifyname2 type string {
+ indexing: summary | attribute newname2 | index
+ # index-to: newname2
+ }
+
+ field withstaticrankname type string {
+ indexing: summary | attribute | index | attribute someothername
+ }
+ }
+}
diff --git a/config-model/src/test/examples/attributeposition.sd b/config-model/src/test/examples/attributeposition.sd
new file mode 100755
index 00000000000..97f2de16cb9
--- /dev/null
+++ b/config-model/src/test/examples/attributeposition.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search atributeposition {
+ document attributeposition {
+
+ field multipos2d type array<string> {
+ indexing: summary | index | attribute | to_pos
+ }
+
+ field pos2d type string {
+ indexing: summary | index | attribute | to_pos
+ }
+
+ }
+}
diff --git a/config-model/src/test/examples/attributeproperties1.sd b/config-model/src/test/examples/attributeproperties1.sd
new file mode 100644
index 00000000000..5fbff8c8491
--- /dev/null
+++ b/config-model/src/test/examples/attributeproperties1.sd
@@ -0,0 +1,21 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search bolding {
+ document test {
+
+ # Setting attribute properties for a non-existent attribute should fail
+ field batchid type int {
+ indexing: summary | index
+ attribute {
+ prefetch
+ }
+ }
+
+ # ... but this is OK
+ field anotherbatchid type int {
+ indexing: summary | index | attribute
+ attribute {
+ prefetch
+ }
+ }
+ }
+}
diff --git a/config-model/src/test/examples/attributeproperties2.sd b/config-model/src/test/examples/attributeproperties2.sd
new file mode 100644
index 00000000000..889bb2d01af
--- /dev/null
+++ b/config-model/src/test/examples/attributeproperties2.sd
@@ -0,0 +1,27 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search bolding {
+ document bolding {
+
+ # This is how it usually should be
+ field anotherbatchid type int {
+ indexing: summary | index | attribute
+ attribute {
+ prefetch
+ }
+ attribute: huge
+ }
+
+ # The attribute is created in the next field
+ field bar type int {
+ indexing: summary | index
+ attribute {
+ prefetch
+ }
+ }
+
+ # Creates attribute for the previous field
+ field foo type int {
+ indexing: input bar | attribute bar
+ }
+ }
+}
diff --git a/config-model/src/test/examples/attributesettings.sd b/config-model/src/test/examples/attributesettings.sd
new file mode 100644
index 00000000000..f9803028d6b
--- /dev/null
+++ b/config-model/src/test/examples/attributesettings.sd
@@ -0,0 +1,71 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search attributesettings {
+
+ document attributesettings {
+
+ field f1 type long {
+ indexing: attribute
+ attribute: huge
+ }
+
+ field f2 type long {
+ indexing: attribute
+ attribute {
+ fast-search
+ alias: f2alias
+ }
+ }
+
+ field f3 type long {
+ indexing: attribute f3
+ attribute {
+ alias f3: f3alias
+ }
+ }
+
+ field f4 type weightedset<string> {
+ weightedset: remove-if-zero
+ weightedset: create-if-nonexistent
+ indexing: attribute
+ }
+
+ field f5 type weightedset<string> {
+ indexing: attribute
+ weightedset: remove-if-zero
+ weightedset: create-if-nonexistent
+ }
+
+ field f6 type weightedset<string> {
+ weightedset: remove-if-zero
+ indexing: attribute
+ weightedset: create-if-nonexistent
+ }
+
+ field f7 type weightedset<string> {
+ indexing: attribute
+ weightedset: create-if-nonexistent
+ }
+
+ field f8 type weightedset<string> {
+ weightedset: create-if-nonexistent
+ indexing: attribute
+ }
+
+ field f9 type weightedset<string> {
+ indexing: attribute
+ weightedset: remove-if-zero
+ }
+
+ field f10 type weightedset<string> {
+ weightedset: remove-if-zero
+ indexing: attribute
+ }
+
+ field fast_access type int {
+ indexing: attribute
+ attribute: fast-access
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/attributesexactmatch.sd b/config-model/src/test/examples/attributesexactmatch.sd
new file mode 100644
index 00000000000..b0203681b3e
--- /dev/null
+++ b/config-model/src/test/examples/attributesexactmatch.sd
@@ -0,0 +1,54 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+
+ field color type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute
+ }
+
+ field drummer type string {
+ indexing: attribute
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute
+ }
+
+ field genre type string {
+ # index-to: foo
+ }
+
+ field trumpetist type string {
+ indexing: attribute | index
+ }
+
+ field saxophonist type string {
+ indexing: summary
+ }
+
+ field flutist type string {
+ indexing: attribute | index
+ match {
+ token
+ }
+ }
+ }
+
+ field saxophonist_arr type array<string> {
+ indexing: input saxophonist | split ";" | attribute
+ }
+}
diff --git a/config-model/src/test/examples/badstruct.sd b/config-model/src/test/examples/badstruct.sd
new file mode 100755
index 00000000000..cd6fbff82e7
--- /dev/null
+++ b/config-model/src/test/examples/badstruct.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search foo {
+
+ struct int {
+ field f type float {}
+ }
+
+ document foo {
+ field i type int {}
+ }
+}
diff --git a/config-model/src/test/examples/casing.sd b/config-model/src/test/examples/casing.sd
new file mode 100644
index 00000000000..70f60562f97
--- /dev/null
+++ b/config-model/src/test/examples/casing.sd
@@ -0,0 +1,64 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+
+ field Color type string {
+ indexing: index
+ # index-to: color
+ alias color: Colour
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute
+ }
+
+ field Drummer type string {
+ indexing: attribute
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute
+ }
+
+ field Genre type string {
+ indexing: index
+ # index-to: Foo
+ alias Foo: sjanger
+ }
+
+ field Price type string {
+ indexing: index
+ alias: Cost
+ }
+
+ field Trumpetist type string {
+ indexing: attribute | index
+ }
+
+ field Saxophonist type string {
+ indexing: summary | attribute Saxophonist
+ }
+
+ field TenorSaxophonist type array<string> {
+ indexing: summary | attribute
+ }
+
+ field Flutist type string {
+ indexing: attribute | index Flutist
+ match {
+ token
+ }
+ }
+
+ }
+}
diff --git a/config-model/src/test/examples/child.sd b/config-model/src/test/examples/child.sd
new file mode 100644
index 00000000000..48b523b2b44
--- /dev/null
+++ b/config-model/src/test/examples/child.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search child {
+
+ document child inherits parent.1 {
+
+ field childs type string {
+ indexing: index
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/comment.sd b/config-model/src/test/examples/comment.sd
new file mode 100644
index 00000000000..096cbe72e55
--- /dev/null
+++ b/config-model/src/test/examples/comment.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search comment {
+
+ document comment {
+
+ field a type string {
+ indexing: summary | index # How should this field be indexed
+ }
+ }
+}
diff --git a/config-model/src/test/examples/desktop.sd b/config-model/src/test/examples/desktop.sd
new file mode 100644
index 00000000000..e99c5f48193
--- /dev/null
+++ b/config-model/src/test/examples/desktop.sd
@@ -0,0 +1,108 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A search definition of medium complexity
+search desktop {
+
+ # A document with some fields.
+ # The fields implicitly defines some indices, summary fields and attributes
+ document desktop inherits product {
+
+ field title type text {
+ mandatory: true
+ indexing: summary | attribute | index_text
+ # index-to: title, default
+
+ rank-weight: 300
+ rank-type: identity
+ }
+
+ field manufacturer type text {
+ indexing: summary | attribute | index
+ # index-to: manufacturer, default
+ alias: producer, brand
+
+ rank-type: identity
+ rank-weight:200
+ }
+
+ field description type text {
+ indexing: summary | index
+
+ rank-type: about
+ rank-weight: 100
+ result-transform: dynamicteaser
+ }
+
+ field category type text {
+ indexing: index
+ # index-to: category, default
+ rank-weight: 50
+ }
+
+ field minprice type int {
+ indexing: summary | attribute | index
+ index-decimals: 2
+
+ rank-type: simple
+ weight: 30
+ staticrankbits: 16
+ }
+
+ field someorder type int {
+ indexing: attribute someorderranking
+ staticrankbits someorderranking: 32
+ }
+
+ # index_url implicitly defines some fields not contained in the document (contexts)
+ # If attributes needs to be set on these, it can be done by explicitly listing
+ # the fields outside documents (show).
+ # I think we should maybe allow setting such field attributes inside the parent
+ # field as well for convenience. Both is shown.
+ field url type url {
+ # Should index mean index_url for url type fields?
+ indexing: summary | index_url
+ parse-mode: url # Must be default for url types, but shown here anyway
+
+ rank-type: link
+ }
+
+ }
+
+ field category_arr type array<text> {
+ indexing: input category | split ";" | attribute category_arr
+ }
+
+ # Overriding some url field setting from rank-type link
+ field url.host {
+ weight: 1000
+ }
+
+ # Setting an attribute on a non-field index
+ # This is redunant, as default is default
+ index default {
+ default: true
+ }
+
+ # Optionally specify a desire to use a shared dictionary ("catalogs")
+ shared-dictionary: normal, title, manufacturer
+
+ # Optionally set rank values for all indices
+ # Default is the name of the default one
+ # Rank settings from individual fields can be selectively overridden
+ rankprofile default {
+ firstocc-boost text: 200
+ }
+
+ # Another rank profile
+ rankprofile predefinedorder {
+ dynamicranking: off
+ attribute: someorder
+ }
+
+ # Some experimental ranking changes
+ rankprofile experimental inherits default {
+ firstocc-boost text: 300
+ }
+
+}
+
+
diff --git a/config-model/src/test/examples/documentidinsummary.sd b/config-model/src/test/examples/documentidinsummary.sd
new file mode 100644
index 00000000000..9cb72fa6390
--- /dev/null
+++ b/config-model/src/test/examples/documentidinsummary.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search documentidinsummary {
+ document documentidinsummary {
+ field a type string { }
+ }
+ document-summary withid {
+ summary w type string { source: documentid }
+ }
+}
diff --git a/config-model/src/test/examples/documents.sd b/config-model/src/test/examples/documents.sd
new file mode 100644
index 00000000000..a4f4e2fb54d
--- /dev/null
+++ b/config-model/src/test/examples/documents.sd
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Files can also contain documents only.
+# These may be inherited by other document definitions, or may
+# just represent some stored data
+document one {
+
+ field title type string {
+ }
+
+ field other type string {
+ }
+
+}
+
+document two {
+
+ field something type string {
+ }
+
+}
diff --git a/config-model/src/test/examples/duplicatenamesindoc.sd b/config-model/src/test/examples/duplicatenamesindoc.sd
new file mode 100644
index 00000000000..acbe37b30cb
--- /dev/null
+++ b/config-model/src/test/examples/duplicatenamesindoc.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search duplicatenamesindoc {
+document {
+ field foo type int {
+ indexing: attribute
+ }
+
+ field fOo type string {
+ indexing: index
+ }
+}
+}
diff --git a/config-model/src/test/examples/duplicatenamesinsearchdifferenttype.sd b/config-model/src/test/examples/duplicatenamesinsearchdifferenttype.sd
new file mode 100644
index 00000000000..e5557cf746e
--- /dev/null
+++ b/config-model/src/test/examples/duplicatenamesinsearchdifferenttype.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search duplicatenamesinsearch {
+document {
+ field grpphotoids64 type string { }
+}
+
+field grpphotoids64 type array<long> {
+ indexing: input grpphotoids64 | split " " | for_each {
+ base64decode } | attribute
+ }
+
+}
diff --git a/config-model/src/test/examples/fieldoftypedocument.cfg b/config-model/src/test/examples/fieldoftypedocument.cfg
new file mode 100644
index 00000000000..9ce928381ec
--- /dev/null
+++ b/config-model/src/test/examples/fieldoftypedocument.cfg
@@ -0,0 +1,74 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id -1344444812
+datatype[1].structtype[0].name "book.header"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "soundtrack"
+datatype[1].structtype[0].field[0].datatype 1412693671
+datatype[1].structtype[0].field[1].name "rankfeatures"
+datatype[1].structtype[0].field[1].datatype 2
+datatype[1].structtype[0].field[2].name "summaryfeatures"
+datatype[1].structtype[0].field[2].datatype 2
+datatype[2].id -820813431
+datatype[2].structtype[0].name "book.body"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[3].id -1383388565
+datatype[3].documenttype[0].name "book"
+datatype[3].documenttype[0].version 0
+datatype[3].documenttype[0].inherits[0].name "document"
+datatype[3].documenttype[0].inherits[0].version 0
+datatype[3].documenttype[0].headerstruct -1344444812
+datatype[3].documenttype[0].bodystruct -820813431
+datatype[3].documenttype[0].fieldsets{[document]}.fields[0] "soundtrack"
+datatype[4].id -1910204744
+datatype[4].structtype[0].name "music.header"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[4].structtype[0].field[0].name "intfield"
+datatype[4].structtype[0].field[0].datatype 0
+datatype[4].structtype[0].field[1].name "stringfield"
+datatype[4].structtype[0].field[1].datatype 2
+datatype[4].structtype[0].field[2].name "longfield"
+datatype[4].structtype[0].field[2].datatype 4
+datatype[4].structtype[0].field[3].name "rankfeatures"
+datatype[4].structtype[0].field[3].datatype 2
+datatype[4].structtype[0].field[4].name "summaryfeatures"
+datatype[4].structtype[0].field[4].datatype 2
+datatype[5].id 993120973
+datatype[5].structtype[0].name "music.body"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[6].id 1412693671
+datatype[6].documenttype[0].name "music"
+datatype[6].documenttype[0].version 0
+datatype[6].documenttype[0].inherits[0].name "document"
+datatype[6].documenttype[0].inherits[0].version 0
+datatype[6].documenttype[0].headerstruct -1910204744
+datatype[6].documenttype[0].bodystruct 993120973
+datatype[6].documenttype[0].fieldsets{[document]}.fields[0] "intfield"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[1] "longfield"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[2] "stringfield"
diff --git a/config-model/src/test/examples/fieldoftypedocument.sd b/config-model/src/test/examples/fieldoftypedocument.sd
new file mode 100644
index 00000000000..307ecb2ae1d
--- /dev/null
+++ b/config-model/src/test/examples/fieldoftypedocument.sd
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search book {
+ document book {
+ field soundtrack type music {}
+ }
+}
diff --git a/config-model/src/test/examples/illegalidentifiers/alias.sd b/config-model/src/test/examples/illegalidentifiers/alias.sd
new file mode 100644
index 00000000000..f5aafe50591
--- /dev/null
+++ b/config-model/src/test/examples/illegalidentifiers/alias.sd
@@ -0,0 +1,38 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document music {
+
+ field color type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute a | attribute b
+ }
+
+ field drummer type string {
+ indexing: index
+ alias: or
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute
+ }
+
+ field genre type string {
+ # index-to: foo
+ }
+ }
+
+}
diff --git a/config-model/src/test/examples/illegalidentifiers/doctypename.sd b/config-model/src/test/examples/illegalidentifiers/doctypename.sd
new file mode 100644
index 00000000000..ac1fb5680b3
--- /dev/null
+++ b/config-model/src/test/examples/illegalidentifiers/doctypename.sd
@@ -0,0 +1,49 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document and {
+ field artist type string {
+ indexing: attribute a | attribute b
+ }
+ }
+
+ document music {
+
+ field color type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute a | attribute b
+ }
+
+ field drummer type string {
+ indexing: attribute
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute
+ }
+
+ field genre type string {
+ # index-to: foo
+ }
+ }
+
+ document true {
+ field name type string {
+ indexing: index | summary
+ }
+ }
+
+}
diff --git a/config-model/src/test/examples/illegalidentifiers/fieldname.sd b/config-model/src/test/examples/illegalidentifiers/fieldname.sd
new file mode 100644
index 00000000000..fcf4fca2d94
--- /dev/null
+++ b/config-model/src/test/examples/illegalidentifiers/fieldname.sd
@@ -0,0 +1,37 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document music {
+
+ field color type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute a | attribute b
+ }
+
+ field not type string {
+ indexing: attribute
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute
+ }
+
+ field genre type string {
+ # index-to: foo
+ }
+ }
+
+}
diff --git a/config-model/src/test/examples/illegalidentifiers/rankprofile.sd b/config-model/src/test/examples/illegalidentifiers/rankprofile.sd
new file mode 100644
index 00000000000..82e9fc8f000
--- /dev/null
+++ b/config-model/src/test/examples/illegalidentifiers/rankprofile.sd
@@ -0,0 +1,41 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document music {
+
+ field color type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute a | attribute b
+ }
+
+ field drummer type string {
+ indexing: attribute
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute | summary
+ }
+
+ field genre type string {
+ # index-to: foo
+ }
+ }
+
+ rank-profile false inherits default {
+ }
+
+
+}
diff --git a/config-model/src/test/examples/illegalidentifiers/searchname.sd b/config-model/src/test/examples/illegalidentifiers/searchname.sd
new file mode 100644
index 00000000000..07c370b6432
--- /dev/null
+++ b/config-model/src/test/examples/illegalidentifiers/searchname.sd
@@ -0,0 +1,37 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search true {
+
+ document music {
+
+ field color type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute a | attribute b
+ }
+
+ field drummer type string {
+ indexing: attribute
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute
+ }
+
+ field genre type string {
+ # index-to: foo
+ }
+ }
+
+}
diff --git a/config-model/src/test/examples/illegalidentifiers/summaryclass.sd b/config-model/src/test/examples/illegalidentifiers/summaryclass.sd
new file mode 100644
index 00000000000..cd91216a137
--- /dev/null
+++ b/config-model/src/test/examples/illegalidentifiers/summaryclass.sd
@@ -0,0 +1,38 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+
+ document music {
+
+ field color type string {
+ indexing: index
+ match {
+ exact
+ }
+ }
+
+ field artist type string {
+ indexing: attribute a | attribute b
+ }
+
+ field drummer type string {
+ indexing: attribute
+ }
+
+ field guitarist type string {
+ indexing: attribute
+ match {
+ token
+ }
+ }
+
+ field title type string {
+ indexing: index | attribute | summary
+ summary-to: id
+ }
+
+ field genre type string {
+ # index-to: foo
+ }
+ }
+
+}
diff --git a/config-model/src/test/examples/implicitsummaries_attribute.sd b/config-model/src/test/examples/implicitsummaries_attribute.sd
new file mode 100644
index 00000000000..3f9074c3fa5
--- /dev/null
+++ b/config-model/src/test/examples/implicitsummaries_attribute.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search implicitsummaries_attribute {
+ document implicitsummaries_attribute {
+ field foo type string {
+ indexing: attribute | summary
+ summary-to: bar
+ }
+ field baz type position {
+ indexing: attribute | summary
+ summary-to: cox
+ }
+ }
+}
diff --git a/config-model/src/test/examples/implicitsummaryfields.sd b/config-model/src/test/examples/implicitsummaryfields.sd
new file mode 100644
index 00000000000..3168496bdd4
--- /dev/null
+++ b/config-model/src/test/examples/implicitsummaryfields.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search implicitsummaryfields {
+ document implicitsummaryfields {
+ field foo type string {
+ indexing: index
+ }
+ }
+}
+
diff --git a/config-model/src/test/examples/importing.sd b/config-model/src/test/examples/importing.sd
new file mode 100644
index 00000000000..85c44789f47
--- /dev/null
+++ b/config-model/src/test/examples/importing.sd
@@ -0,0 +1,34 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# This is what a search definition would look like
+# when using some already defined documents to be
+# indexed in a new way
+# TODO: Will probably not do it this way but instead just override fields?
+# Or both?
+search textindex {
+
+ use-document: desktop
+ use-document: pc
+
+ indexing desktop {
+ field desktopname | summary name | index default;
+ field title | summary | index default;
+ field description | summary | index default;
+ }
+
+ indexing pc {
+ field name | summary | index default;
+ field title | summary | index default;
+ field description | summary | index default;
+ }
+
+ # A field which does not exist in a document
+ field totalnoise type int {
+ indexing: field fannoise + field othernoise | index
+ }
+
+ rank-profile other {
+ # Refer to that field just as others...
+ and-boost totalnoise: 300
+ }
+
+}
diff --git a/config-model/src/test/examples/incorrectrankingexpressionfileref.sd b/config-model/src/test/examples/incorrectrankingexpressionfileref.sd
new file mode 100644
index 00000000000..888cb7c6184
--- /dev/null
+++ b/config-model/src/test/examples/incorrectrankingexpressionfileref.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search incorrectrankingexpressionfileref {
+
+ document incorrectrankingexpressionfileref {
+ }
+
+ rank-profile default {
+ first-phase {
+ expression: file:wrongending.expr
+ }
+ }
+
+}
diff --git a/config-model/src/test/examples/incorrectsummarytypes.sd b/config-model/src/test/examples/incorrectsummarytypes.sd
new file mode 100644
index 00000000000..70278cea0a7
--- /dev/null
+++ b/config-model/src/test/examples/incorrectsummarytypes.sd
@@ -0,0 +1,19 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search incorrectsummarytypes {
+
+ document incorrectsummarytypes {
+
+ field somestring type string {
+ indexing: summary
+ }
+
+ }
+
+ document-summary incorrect {
+
+ summary somestring type int {
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/indexing.sd b/config-model/src/test/examples/indexing.sd
new file mode 100644
index 00000000000..1c76afa8daf
--- /dev/null
+++ b/config-model/src/test/examples/indexing.sd
@@ -0,0 +1,18 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A configuration doing a lot of IL magic in indexing statement
+search indexing {
+ document indexing {
+
+ }
+ field number type int {
+ indexing {
+ 10000 | set_var foo;
+ 5000 | set_var bar;
+ get_var foo + get_var bar | set_var addition;
+ get_var foo - get_var bar | set_var subtraction;
+ get_var foo * get_var bar | set_var multiplication;
+ get_var foo / get_var bar | set_var division;
+ get_var foo % get_var bar | set_var modulus;
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_attribute_changed.sd b/config-model/src/test/examples/indexing_attribute_changed.sd
new file mode 100644
index 00000000000..a68b0b5dc8d
--- /dev/null
+++ b/config-model/src/test/examples/indexing_attribute_changed.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_attribute_changed {
+ document indexing_attribute_changed {
+ field foo type string {
+ indexing: summary | lowercase | attribute
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_attribute_other.sd b/config-model/src/test/examples/indexing_attribute_other.sd
new file mode 100644
index 00000000000..f554ca33490
--- /dev/null
+++ b/config-model/src/test/examples/indexing_attribute_other.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_attribute_other {
+ document indexing_attribute_other {
+ field foo type string {
+ indexing: attribute bar
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_extra.sd b/config-model/src/test/examples/indexing_extra.sd
new file mode 100644
index 00000000000..cf891044c06
--- /dev/null
+++ b/config-model/src/test/examples/indexing_extra.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_extra {
+ document indexing_extra {
+ field my_index type string {
+ indexing: index | summary
+ }
+ field my_input type string {
+ }
+ }
+ field my_extra type string {
+ indexing: input my_input | index | summary
+ }
+}
diff --git a/config-model/src/test/examples/indexing_extra_field_input_extra_field.sd b/config-model/src/test/examples/indexing_extra_field_input_extra_field.sd
new file mode 100644
index 00000000000..cfc8f90b509
--- /dev/null
+++ b/config-model/src/test/examples/indexing_extra_field_input_extra_field.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_extra_field_input_extra_field {
+ document indexing_extra_field_input_extra_field {
+
+ }
+ field foo type string {
+
+ }
+ field bar type string {
+ indexing: input bar | index
+ }
+}
diff --git a/config-model/src/test/examples/indexing_extra_field_input_implicit.sd b/config-model/src/test/examples/indexing_extra_field_input_implicit.sd
new file mode 100644
index 00000000000..d117e90e897
--- /dev/null
+++ b/config-model/src/test/examples/indexing_extra_field_input_implicit.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_extra_field_input_implicit {
+ document indexing_extra_field_input_implicit {
+
+ }
+ field foo type string {
+ indexing: index
+ }
+}
diff --git a/config-model/src/test/examples/indexing_extra_field_input_null.sd b/config-model/src/test/examples/indexing_extra_field_input_null.sd
new file mode 100644
index 00000000000..1557c9eca53
--- /dev/null
+++ b/config-model/src/test/examples/indexing_extra_field_input_null.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_extra_field_input_null {
+ document indexing_extra_field_input_null {
+
+ }
+ field foo type string {
+ indexing: input | index
+ }
+}
diff --git a/config-model/src/test/examples/indexing_extra_field_input_self.sd b/config-model/src/test/examples/indexing_extra_field_input_self.sd
new file mode 100644
index 00000000000..b78a2674512
--- /dev/null
+++ b/config-model/src/test/examples/indexing_extra_field_input_self.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_extra_field_input_self {
+ document indexing_extra_field_input_self {
+
+ }
+ field foo type string {
+ indexing: input foo | index
+ }
+}
diff --git a/config-model/src/test/examples/indexing_index_changed.sd b/config-model/src/test/examples/indexing_index_changed.sd
new file mode 100644
index 00000000000..ea268f90635
--- /dev/null
+++ b/config-model/src/test/examples/indexing_index_changed.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_index_changed {
+ document indexing_index_changed {
+ field foo type string {
+ indexing: attribute | lowercase | index
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_index_other.sd b/config-model/src/test/examples/indexing_index_other.sd
new file mode 100644
index 00000000000..32c1395815f
--- /dev/null
+++ b/config-model/src/test/examples/indexing_index_other.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_index_other {
+ document indexing_index_other {
+ field foo type string {
+ indexing: index bar
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_input_other_field.sd b/config-model/src/test/examples/indexing_input_other_field.sd
new file mode 100644
index 00000000000..1f7782e510d
--- /dev/null
+++ b/config-model/src/test/examples/indexing_input_other_field.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_input_other_field {
+ document indexing_input_other_field {
+ field foo type string {
+
+ }
+ field bar type string {
+ indexing: input foo | attribute | index | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_invalid_expression.sd b/config-model/src/test/examples/indexing_invalid_expression.sd
new file mode 100644
index 00000000000..406ca280b8d
--- /dev/null
+++ b/config-model/src/test/examples/indexing_invalid_expression.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_invalid_expression {
+ document indexing_invalid_expression {
+ field product type string {
+ indexing: input product | summary product | foo | index product
+ }
+ }
+}
+
diff --git a/config-model/src/test/examples/indexing_modify_field_no_output.sd b/config-model/src/test/examples/indexing_modify_field_no_output.sd
new file mode 100644
index 00000000000..e1f4e6c3a5b
--- /dev/null
+++ b/config-model/src/test/examples/indexing_modify_field_no_output.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_modify_field_no_output {
+ document indexing_modify_field_no_output {
+ field foo type string {
+ indexing: lowercase | echo
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_multiline_output_conflict.sd b/config-model/src/test/examples/indexing_multiline_output_conflict.sd
new file mode 100644
index 00000000000..de17efa00ae
--- /dev/null
+++ b/config-model/src/test/examples/indexing_multiline_output_conflict.sd
@@ -0,0 +1,21 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_multiline_output_confict {
+ document indexing_multiline_output_confict {
+ field foo type string {
+
+ }
+ field bar type string {
+
+ }
+ field baz type string {
+
+ }
+ }
+ field cox type string {
+ indexing {
+ input foo | attribute;
+ input bar | index;
+ input baz | summary;
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_output_conflict.sd b/config-model/src/test/examples/indexing_output_conflict.sd
new file mode 100644
index 00000000000..b7e0859a2b7
--- /dev/null
+++ b/config-model/src/test/examples/indexing_output_conflict.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_output_confict {
+ document indexing_output_confict {
+ field foo type string {
+
+ }
+ }
+ field bar type string {
+ indexing: input foo | attribute | lowercase | index
+ }
+}
diff --git a/config-model/src/test/examples/indexing_output_other_field.sd b/config-model/src/test/examples/indexing_output_other_field.sd
new file mode 100644
index 00000000000..470ac0db161
--- /dev/null
+++ b/config-model/src/test/examples/indexing_output_other_field.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_output_other_field {
+ document indexing_output_other_field {
+ field foo type string {
+ indexing: index bar
+ }
+ }
+ field bar type string {
+
+ }
+}
diff --git a/config-model/src/test/examples/indexing_summary_changed.sd b/config-model/src/test/examples/indexing_summary_changed.sd
new file mode 100644
index 00000000000..bd7b225b8bc
--- /dev/null
+++ b/config-model/src/test/examples/indexing_summary_changed.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_summary_fail {
+ document indexing_summary_fail {
+ field foo type string {
+ indexing: index | lowercase | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexing_summary_other.sd b/config-model/src/test/examples/indexing_summary_other.sd
new file mode 100644
index 00000000000..6529ceb1012
--- /dev/null
+++ b/config-model/src/test/examples/indexing_summary_other.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexing_summary_other {
+ document indexing_summary_other {
+ field foo type string {
+ indexing: summary bar
+ }
+ }
+}
diff --git a/config-model/src/test/examples/indexrewrite.sd b/config-model/src/test/examples/indexrewrite.sd
new file mode 100644
index 00000000000..fdba8a0d2aa
--- /dev/null
+++ b/config-model/src/test/examples/indexrewrite.sd
@@ -0,0 +1,19 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexrewrite {
+ document indexrewrite {
+ field title_src type string {
+
+ }
+ }
+ field title type string {
+ indexing: input title_src | lowercase | normalize | tokenize | index
+ # index-to: title, titleabstract, default
+ indexing-rewrite: none
+ rank-type: about
+ stemming: none
+ alias: headline
+ }
+ field title_s type string {
+ indexing: input title_src | summary
+ }
+}
diff --git a/config-model/src/test/examples/indexsettings.sd b/config-model/src/test/examples/indexsettings.sd
new file mode 100644
index 00000000000..c5eca80c1a0
--- /dev/null
+++ b/config-model/src/test/examples/indexsettings.sd
@@ -0,0 +1,25 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search indexsettings {
+
+ stemming: shortest
+
+ document indexsettings {
+
+ field allstemmed type string {
+ stemming: all
+ }
+
+ field usingdefault type string {
+ }
+
+ field notstemmed type string {
+ stemming: none
+ }
+
+ field multiplestems type string {
+ stemming: multiple
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/integerindex2attribute.sd b/config-model/src/test/examples/integerindex2attribute.sd
new file mode 100644
index 00000000000..1191a8f005a
--- /dev/null
+++ b/config-model/src/test/examples/integerindex2attribute.sd
@@ -0,0 +1,32 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search integerindex2attribute {
+ document integerindex2attribute {
+
+ field s1 type string {
+ indexing: index
+ }
+ field s2 type string {
+ indexing: index | attribute
+ }
+ field as1 type array<string> {
+ indexing: index
+ }
+ field as2 type array<string> {
+ indexing: index | attribute
+ }
+
+ field i1 type int {
+ indexing: index
+ }
+ field i2 type int {
+ indexing: index | attribute
+ }
+ field ai1 type array<int> {
+ indexing: index
+ }
+ field ai2 type array<int> {
+ indexing: index | attribute
+ }
+
+ }
+}
diff --git a/config-model/src/test/examples/invalid_sd_construct.sd b/config-model/src/test/examples/invalid_sd_construct.sd
new file mode 100644
index 00000000000..df26f8b60ef
--- /dev/null
+++ b/config-model/src/test/examples/invalid_sd_construct.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search lodging {
+ document lodging {
+ field latlong type string {
+ indexing: attribute location (position, 2)
+ }
+ }
+}
diff --git a/config-model/src/test/examples/invalid_sd_junk_at_end.sd b/config-model/src/test/examples/invalid_sd_junk_at_end.sd
new file mode 100644
index 00000000000..6b75b7603ac
--- /dev/null
+++ b/config-model/src/test/examples/invalid_sd_junk_at_end.sd
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search junkatend {
+ document {
+ field title type string {
+ indexing: attribute
+ }
+ }
+}
+
+}
diff --git a/config-model/src/test/examples/invalid_sd_lexical_error.sd b/config-model/src/test/examples/invalid_sd_lexical_error.sd
new file mode 100644
index 00000000000..0fd2545ea4c
--- /dev/null
+++ b/config-model/src/test/examples/invalid_sd_lexical_error.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search twitter {
+
+ document twitter {
+
+ field user_favorited type long {
+ indexing: summary | attribute
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/invalid_sd_no_closing_bracket.sd b/config-model/src/test/examples/invalid_sd_no_closing_bracket.sd
new file mode 100644
index 00000000000..6fb91303f62
--- /dev/null
+++ b/config-model/src/test/examples/invalid_sd_no_closing_bracket.sd
@@ -0,0 +1,7 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search noclosingbracket {
+ document {
+ field title type string {
+ indexing: attribute
+ }
+ }
diff --git a/config-model/src/test/examples/invalidimplicitsummarysource.sd b/config-model/src/test/examples/invalidimplicitsummarysource.sd
new file mode 100644
index 00000000000..2c6983edff1
--- /dev/null
+++ b/config-model/src/test/examples/invalidimplicitsummarysource.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search invalidsummarysource {
+ document invalidsummarysource {
+ field foo type string {
+ indexing: summary
+ }
+ }
+ document-summary baz {
+ summary cox type string { }
+ }
+}
diff --git a/config-model/src/test/examples/invalidngram1.sd b/config-model/src/test/examples/invalidngram1.sd
new file mode 100644
index 00000000000..4d3295d89f5
--- /dev/null
+++ b/config-model/src/test/examples/invalidngram1.sd
@@ -0,0 +1,17 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search invalidngram1 {
+
+ document invalidngram1 {
+
+ field invalid type string {
+ indexing: index | summary
+ summary: dynamic
+ match {
+ text
+ gram-size:1
+ }
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/invalidngram2.sd b/config-model/src/test/examples/invalidngram2.sd
new file mode 100644
index 00000000000..64cb302abb5
--- /dev/null
+++ b/config-model/src/test/examples/invalidngram2.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search invalidngram2 {
+
+ document invalidngram2 {
+
+ field invalid type string {
+ indexing: index | summary
+ summary: dynamic
+ match {
+ gram-size:1
+ }
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/invalidngram3.sd b/config-model/src/test/examples/invalidngram3.sd
new file mode 100644
index 00000000000..94bf4d1a365
--- /dev/null
+++ b/config-model/src/test/examples/invalidngram3.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search invalidngram3 {
+
+ document invalidngram3 {
+
+ field invalid type string {
+ indexing: attribute | summary
+ match {
+ gram
+ }
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/invalidselfreferringsummary.sd b/config-model/src/test/examples/invalidselfreferringsummary.sd
new file mode 100644
index 00000000000..154b4742b8d
--- /dev/null
+++ b/config-model/src/test/examples/invalidselfreferringsummary.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search invalidselfreferringsummary {
+ document invalidselfreferringsummary {
+ field a type string { }
+ }
+ document-summary withid {
+ summary w type string { source: w }
+ }
+}
diff --git a/config-model/src/test/examples/invalidsummarysource.sd b/config-model/src/test/examples/invalidsummarysource.sd
new file mode 100644
index 00000000000..4d29fc249c2
--- /dev/null
+++ b/config-model/src/test/examples/invalidsummarysource.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search invalidsummarysource {
+ document invalidsummarysource {
+ field foo type string {
+ indexing: summary
+ }
+ }
+ document-summary baz {
+ summary cox type string { source: nonexistingfield }
+ }
+}
diff --git a/config-model/src/test/examples/matchphase/non_existing_attribute.sd b/config-model/src/test/examples/matchphase/non_existing_attribute.sd
new file mode 100644
index 00000000000..e39087109b7
--- /dev/null
+++ b/config-model/src/test/examples/matchphase/non_existing_attribute.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search test {
+ document test {
+ field foo type int {
+ indexing: summary
+ }
+ }
+ rank-profile default {
+ match-phase {
+ attribute: foo
+ max-hits: 100
+ }
+ }
+}
diff --git a/config-model/src/test/examples/matchphase/non_fast_search_attribute.sd b/config-model/src/test/examples/matchphase/non_fast_search_attribute.sd
new file mode 100644
index 00000000000..23472e1049f
--- /dev/null
+++ b/config-model/src/test/examples/matchphase/non_fast_search_attribute.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search test {
+ document test {
+ field foo type int {
+ indexing: attribute
+ }
+ }
+ rank-profile default {
+ match-phase {
+ attribute: foo
+ max-hits: 100
+ }
+ }
+}
diff --git a/config-model/src/test/examples/matchphase/wrong_collection_type_attribute.sd b/config-model/src/test/examples/matchphase/wrong_collection_type_attribute.sd
new file mode 100644
index 00000000000..10b444bd8cd
--- /dev/null
+++ b/config-model/src/test/examples/matchphase/wrong_collection_type_attribute.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search test {
+ document test {
+ field foo type array<int> {
+ indexing: attribute
+ }
+ }
+ rank-profile default {
+ match-phase {
+ attribute: foo
+ max-hits: 100
+ }
+ }
+}
diff --git a/config-model/src/test/examples/matchphase/wrong_data_type_attribute.sd b/config-model/src/test/examples/matchphase/wrong_data_type_attribute.sd
new file mode 100644
index 00000000000..fe209045592
--- /dev/null
+++ b/config-model/src/test/examples/matchphase/wrong_data_type_attribute.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search test {
+ document test {
+ field foo type string {
+ indexing: attribute
+ }
+ }
+ rank-profile default {
+ match-phase {
+ attribute: foo
+ max-hits: 100
+ }
+ }
+}
diff --git a/config-model/src/test/examples/multiplesummaries.sd b/config-model/src/test/examples/multiplesummaries.sd
new file mode 100644
index 00000000000..86923165527
--- /dev/null
+++ b/config-model/src/test/examples/multiplesummaries.sd
@@ -0,0 +1,33 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search multiplesummaries {
+
+ document multiplesummaries {
+
+ field field1 type weightedset<string> {
+ indexing: summary | attribute
+ }
+
+ field field2 type tag {
+ indexing: summary | attribute
+ }
+
+ field field3 type array<int> {
+ indexing: summary | attribute
+ }
+
+ }
+
+ document-summary other {
+
+ summary field1 type weightedset<string> {
+ }
+
+ summary field2 type tag {
+ }
+
+ summary field3 type array<int> {
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/music.sd b/config-model/src/test/examples/music.sd
new file mode 100644
index 00000000000..9e80e2b7a22
--- /dev/null
+++ b/config-model/src/test/examples/music.sd
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+ document music {
+ field intfield type int {}
+ field stringfield type string {}
+ field longfield type long {}
+ }
diff --git a/config-model/src/test/examples/name-check.sd b/config-model/src/test/examples/name-check.sd
new file mode 100644
index 00000000000..5155d13db71
--- /dev/null
+++ b/config-model/src/test/examples/name-check.sd
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# An entry-level configuration.
+# You can get a reasonable configuration by only configuring
+# a document
+search simple {
+
+ document name-check {
+
+ field title type string {
+ indexing: summary | index
+ }
+
+ # reserved name, should trigger error
+ field sddocname type string {
+ indexing: index
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/nextgen/boldedsummaryfields.sd b/config-model/src/test/examples/nextgen/boldedsummaryfields.sd
new file mode 100644
index 00000000000..770f768d151
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/boldedsummaryfields.sd
@@ -0,0 +1,19 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search boldedsummaryfields {
+ document boldedsummaryfields {
+ field foo type string {
+ indexing: index | summary
+ summary bar {
+ source: foo
+ bolding: on
+ }
+ }
+ field baz type string {
+ indexing: attribute | summary
+ summary cox {
+ source: baz
+ bolding: on
+ }
+ }
+ }
+}
diff --git a/config-model/src/test/examples/nextgen/dynamicsummaryfields.sd b/config-model/src/test/examples/nextgen/dynamicsummaryfields.sd
new file mode 100644
index 00000000000..9ac3623cd43
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/dynamicsummaryfields.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search dynamicsummaryfields {
+ document dynamicsummaryfields {
+ field foo type string {
+ indexing: index | summary
+ summary: dynamic
+ }
+
+ field bar type string {
+ indexing: index | summary
+ bolding: on
+ }
+ }
+}
diff --git a/config-model/src/test/examples/nextgen/extrafield.sd b/config-model/src/test/examples/nextgen/extrafield.sd
new file mode 100644
index 00000000000..973be5c83e0
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/extrafield.sd
@@ -0,0 +1,12 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search extrafield {
+ document extrafield {
+ field foo type int {
+ indexing: index
+ }
+ }
+ field bar type string {
+ indexing: input foo | to_string | index
+ }
+}
+
diff --git a/config-model/src/test/examples/nextgen/implicitstructtypes.sd b/config-model/src/test/examples/nextgen/implicitstructtypes.sd
new file mode 100644
index 00000000000..de5d24810ac
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/implicitstructtypes.sd
@@ -0,0 +1,18 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search implicitstructtypes {
+ document implicitstructtypes {
+ field doc_str type string {
+ indexing: index | summary
+ summary doc_str_sum: full
+ }
+ field doc_uri type uri {
+ indexing: index | summary
+ }
+ }
+ document-summary docsum {
+ summary docsum_str type string {
+ source: doc_str_sum
+ }
+ }
+}
+
diff --git a/config-model/src/test/examples/nextgen/simple.sd b/config-model/src/test/examples/nextgen/simple.sd
new file mode 100644
index 00000000000..6cfbea496d8
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/simple.sd
@@ -0,0 +1,17 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search simple {
+ document simple {
+ field doc_field type string {
+ indexing: index | summary
+ summary doc_field_summary: full
+ }
+ }
+ document-summary explicit_summary {
+ summary summary_field type string {
+ source: doc_field_summary
+ }
+ }
+ field extern_field type string {
+ indexing: input doc_field | index
+ }
+}
diff --git a/config-model/src/test/examples/nextgen/summaryfield.sd b/config-model/src/test/examples/nextgen/summaryfield.sd
new file mode 100644
index 00000000000..7f5c777ddc3
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/summaryfield.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search summaryfield {
+ document summaryfield {
+ field foo type string {
+ indexing: index | summary
+ summary bar: full
+ }
+ }
+ document-summary baz {
+ summary cox type string {
+ source: bar
+ }
+ }
+}
+
diff --git a/config-model/src/test/examples/nextgen/toggleon.sd b/config-model/src/test/examples/nextgen/toggleon.sd
new file mode 100644
index 00000000000..9e118a51bc8
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/toggleon.sd
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search toggleon {
+ document toggleon {
+ field foo type int {
+ indexing: index
+ }
+ }
+}
+
diff --git a/config-model/src/test/examples/nextgen/untransformedsummaryfields.sd b/config-model/src/test/examples/nextgen/untransformedsummaryfields.sd
new file mode 100644
index 00000000000..b6307a60e7f
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/untransformedsummaryfields.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search untransformedsummaryfields {
+ document untransformedsummaryfields {
+ field foo type int {
+ indexing: index | summary
+ }
+ field bar type string {
+ indexing: index | summary
+ bolding: on
+ }
+ field baz type int {
+ indexing: attribute | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/nextgen/unusedfields.sd b/config-model/src/test/examples/nextgen/unusedfields.sd
new file mode 100644
index 00000000000..e9372a4cc8b
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/unusedfields.sd
@@ -0,0 +1,15 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search unusedfields {
+ document unusedfields {
+ field foo type int {
+ indexing: index
+ }
+ field bar type int {
+
+ }
+ }
+ field baz type string {
+
+ }
+}
+
diff --git a/config-model/src/test/examples/nextgen/uri_array.sd b/config-model/src/test/examples/nextgen/uri_array.sd
new file mode 100644
index 00000000000..541e793a99f
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/uri_array.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search uri_array {
+ document uri_array {
+ field my_field type array<uri> {
+ indexing: index | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/nextgen/uri_simple.sd b/config-model/src/test/examples/nextgen/uri_simple.sd
new file mode 100644
index 00000000000..49333ac8677
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/uri_simple.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search uri_simple {
+ document uri_simple {
+ field my_field type uri {
+ indexing: index | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/nextgen/uri_wset.sd b/config-model/src/test/examples/nextgen/uri_wset.sd
new file mode 100644
index 00000000000..f9394d9a6f2
--- /dev/null
+++ b/config-model/src/test/examples/nextgen/uri_wset.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search uri_array {
+ document uri_array {
+ field my_field type weightedset<uri> {
+ indexing: index | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/ngram.sd b/config-model/src/test/examples/ngram.sd
new file mode 100644
index 00000000000..3b7f6401cb6
--- /dev/null
+++ b/config-model/src/test/examples/ngram.sd
@@ -0,0 +1,32 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search ngram {
+
+ document ngram {
+
+ field gram_1 type string {
+ indexing: index | summary
+ summary: dynamic
+ match {
+ gram
+ gram-size:1
+ }
+ }
+
+ field gram_2 type string {
+ indexing: index
+ match: gram # gram-size 2 is default
+ # index-to: gram_2, default
+ }
+
+ field gram_3 type string {
+ indexing: index
+ match {
+ gram
+ gram-size: 3
+ }
+ # index-to: gram_3, default
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/outsidedoc.sd b/config-model/src/test/examples/outsidedoc.sd
new file mode 100644
index 00000000000..e306956e6c0
--- /dev/null
+++ b/config-model/src/test/examples/outsidedoc.sd
@@ -0,0 +1,18 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search outsidedoc {
+
+ index default {
+ prefix
+ alias: default.default
+ }
+
+ document outsidedoc {
+
+ field a type string {
+ indexing: index
+ # index-to: default
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/outsidesummary.sd b/config-model/src/test/examples/outsidesummary.sd
new file mode 100644
index 00000000000..bedef269952
--- /dev/null
+++ b/config-model/src/test/examples/outsidesummary.sd
@@ -0,0 +1,45 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search outsidesummary {
+
+ document-summary other {
+
+ summary sa type string {
+ dynamic
+ source: a
+ }
+
+ summary sa2 type string {
+ full
+ source: a
+ }
+
+ summary a type string {
+ }
+
+ }
+
+ document outsidesummary {
+
+ field a type string {
+ indexing: summary
+ }
+
+ field b type string {
+ indexing: summary
+ summary-to: default, other
+ summary {
+ dynamic
+ }
+ }
+
+ field c type string {
+ indexing: summary
+ summary {
+ dynamic
+ to: other, default
+ }
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/position_array.sd b/config-model/src/test/examples/position_array.sd
new file mode 100644
index 00000000000..a4d1396ec43
--- /dev/null
+++ b/config-model/src/test/examples/position_array.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_array {
+ document position_array {
+ field pos type array<position> {
+ indexing: attribute | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/position_attribute.sd b/config-model/src/test/examples/position_attribute.sd
new file mode 100644
index 00000000000..8e14e289cd3
--- /dev/null
+++ b/config-model/src/test/examples/position_attribute.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_attribute {
+ document position_attribute {
+ field pos type position {
+ indexing: attribute | summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/position_extra.sd b/config-model/src/test/examples/position_extra.sd
new file mode 100644
index 00000000000..0205c8fd437
--- /dev/null
+++ b/config-model/src/test/examples/position_extra.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_extra {
+ document position_extra {
+ field pos_str type string {
+ indexing: summary | index
+ }
+ }
+ field pos_ext type position {
+ indexing: input pos_str | to_pos | attribute | summary
+ }
+}
diff --git a/config-model/src/test/examples/position_index.sd b/config-model/src/test/examples/position_index.sd
new file mode 100644
index 00000000000..a385c55f507
--- /dev/null
+++ b/config-model/src/test/examples/position_index.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_index {
+ document position_index {
+ field pos type position {
+ indexing: index
+ }
+ }
+}
diff --git a/config-model/src/test/examples/position_summary.sd b/config-model/src/test/examples/position_summary.sd
new file mode 100644
index 00000000000..9f68b650113
--- /dev/null
+++ b/config-model/src/test/examples/position_summary.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search position_summary {
+ document position_summary {
+ field pos type position {
+ indexing: summary
+ }
+ }
+}
diff --git a/config-model/src/test/examples/rankingexpressionfunction/rankingexpressionfunction.sd b/config-model/src/test/examples/rankingexpressionfunction/rankingexpressionfunction.sd
new file mode 100644
index 00000000000..f14bfd4efa5
--- /dev/null
+++ b/config-model/src/test/examples/rankingexpressionfunction/rankingexpressionfunction.sd
@@ -0,0 +1,39 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search rankexpression {
+
+ document rankexpression {
+
+ field artist type string {
+ indexing: summary | index
+ }
+
+ field title type string {
+ indexing: summary | index
+ }
+
+ field surl type string {
+ indexing: summary
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ }
+
+ }
+
+ rank-profile macros {
+ macro titlematch$(var1, var2) {
+ expression: file: titlematch
+ }
+
+ macro artistmatch() {
+ expression: 78+closeness(distance)
+ }
+
+ first-phase {
+ expression: 0.8+0.2*titlematch$(4,5)+0.8*titlematch$(7,8)*closeness(distance)
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/rankingexpressionfunction/titlematch.expression b/config-model/src/test/examples/rankingexpressionfunction/titlematch.expression
new file mode 100644
index 00000000000..614a0b51442
--- /dev/null
+++ b/config-model/src/test/examples/rankingexpressionfunction/titlematch.expression
@@ -0,0 +1 @@
+var1*var2+890
diff --git a/config-model/src/test/examples/rankingexpressioninfile/rankingexpressioninfile.sd b/config-model/src/test/examples/rankingexpressioninfile/rankingexpressioninfile.sd
new file mode 100644
index 00000000000..b52699ec8dc
--- /dev/null
+++ b/config-model/src/test/examples/rankingexpressioninfile/rankingexpressioninfile.sd
@@ -0,0 +1,20 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search rankexpression {
+
+ document rankexpression {
+
+ field artist type string {
+ indexing: summary | index
+ # index-to: artist, default
+ }
+
+ }
+
+ rank-profile macros {
+ first-phase {
+ expression: file : a/b/c.sd
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/rankmodifier/literal.sd b/config-model/src/test/examples/rankmodifier/literal.sd
new file mode 100644
index 00000000000..ad3e5da2c12
--- /dev/null
+++ b/config-model/src/test/examples/rankmodifier/literal.sd
@@ -0,0 +1,33 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+
+ field title type string {
+ indexing: summary | index
+ rank {
+ literal
+ }
+ }
+
+ field artist type string {
+ indexing: summary | index
+ }
+
+ field genre type string {
+ indexing: summary | index
+ rank: literal
+ }
+
+ field publisher type string {
+ indexing: summary | index
+ rank: literal
+ }
+
+ field drummer type string {
+ indexing: summary | index
+ rank {
+ literal
+ }
+ }
+ }
+}
diff --git a/config-model/src/test/examples/rankpropvars.sd b/config-model/src/test/examples/rankpropvars.sd
new file mode 100644
index 00000000000..fbe6c52a826
--- /dev/null
+++ b/config-model/src/test/examples/rankpropvars.sd
@@ -0,0 +1,80 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+rank-profile other {
+
+rank-properties {
+ $testvar1 : "foo"
+ $testvar_2:"bar"
+ $testvarOne23: "baz"
+ fieldMatch(title).maxAlternativeSegmentations: 10
+ fieldmatch(title).maxOccurrences: 5
+ fieldMatch(description).maxOccurrences: 20
+}
+
+first-phase {
+ expression:nativeRank
+}
+
+second-phase {
+ expression {
+ if (attribute(artist) == query(testvar1),
+ 0.0 * fieldMatch(title) + 0.0 * attribute(popularity) + 0.0 * fieldMatch(artist),
+ 0.0 * attribute(popularity) + 0.0 * fieldMatch(artist) + 0.0 * fieldMatch(title))
+ }
+}
+
+}
+
+rank-profile another {
+rank-properties {
+ $Testvar1 : "1"
+ $Testvar_4:"4"
+ $testvarFour23: "234234.234"
+ fieldMatch(title).maxAlternativeSegmentations: 45
+ fieldmatch(title).maxOccurrences: 56
+ fieldMatch(description).maxOccurrences: 23
+}
+
+first-phase {
+ expression:nativeRank
+}
+
+second-phase {
+ expression {
+ if (attribute(artist) == query(testvar1),
+ 0.0 * fieldMatch(title) + 0.0 * attribute(popularity) + 0.0 * fieldMatch(artist),
+ 0.0 * attribute(popularity) + 0.0 * fieldMatch(artist) + 0.0 * fieldMatch(title))
+ }
+}
+}
+
+document music {
+
+ field title type string {
+ indexing: index | summary
+ body
+ }
+
+ field artist type string {
+ ## index-to: a
+ indexing: index | summary
+ body
+ }
+
+ field year type int {
+ indexing: attribute | summary
+ ## index-to: y
+ body
+ }
+
+ field url type uri {
+ body
+ }
+
+ field Popularity type string {
+ indexing: attribute | summary
+ body
+ }
+}
+
+}
diff --git a/config-model/src/test/examples/reserved_words_as_field_names.sd b/config-model/src/test/examples/reserved_words_as_field_names.sd
new file mode 100644
index 00000000000..59f4269e340
--- /dev/null
+++ b/config-model/src/test/examples/reserved_words_as_field_names.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search test {
+
+ document test {
+
+ field inline type string {
+
+ }
+
+ field constants type string {
+
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/simple-with-weird-name.sd b/config-model/src/test/examples/simple-with-weird-name.sd
new file mode 100644
index 00000000000..e56650ba4cc
--- /dev/null
+++ b/config-model/src/test/examples/simple-with-weird-name.sd
@@ -0,0 +1,13 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A minimal doc with name which is incompatible with YQL+
+search simple-with-weird-name {
+
+ document simple-with-weird-name {
+
+ field title type string {
+ indexing: summary | index
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/examples/simple.sd b/config-model/src/test/examples/simple.sd
new file mode 100644
index 00000000000..2c5864e522f
--- /dev/null
+++ b/config-model/src/test/examples/simple.sd
@@ -0,0 +1,147 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# An entry-level configuration.
+# You can get a reasonable configuration by only configuring
+# a document
+# ...this has become less and less simple over time actually
+search simple {
+
+ document simple {
+
+ field title type string {
+ indexing: summary | index
+ # index-to: title, default
+ index: prefix
+ rank-type: identity
+ rank-type default: about
+ alias: analias.totitle
+ alias default: analias.todefault
+ alias: aliaz
+ }
+
+ field description type string {
+ # index-to: default, description
+ indexing: summary | index
+ summary: full
+ summary dyndesc: dynamic
+ rank-type: about
+ rank-type default: tags
+ alias: hallo
+ }
+
+ field longdesc type string {
+ indexing: summary
+ summary: full
+ summary longstat: full
+ summary dynlong: dynamic
+ summary dyndesc2: dynamic
+ }
+
+ field chatter type string {
+ indexing: index
+ # index-to: default
+ rank-type: about
+ }
+
+ field category type string {
+ indexing: index
+ rank-type: tags
+ stemming: none
+ normalizing: none
+ header
+ }
+
+ field popularity type int {
+ indexing: attribute
+ }
+
+ field measurement type int {
+ indexing: attribute | summary
+ rank-type: empty
+ # index-to: measurement
+ }
+
+ field smallattribute type array<byte> {
+ indexing: attribute
+ }
+
+ field access type byte { indexing: attribute }
+
+ field categories_src type string {
+
+ }
+
+ field categoriesagain_src type string {
+
+ }
+
+ field exactemento_src type string {
+
+ }
+ }
+
+ field categories type string {
+ indexing: input categories_src | lowercase | normalize | index
+ body
+ }
+
+ field categoriesagain type string {
+ indexing {
+ input categoriesagain_src | lowercase | normalize | index
+ }
+ }
+
+ field exactemento type string { indexing {
+ input exactemento_src | lowercase | index | summary;
+ }}
+
+ field category_arr type array<string> {
+ indexing: input category | split ";" | attribute
+ }
+
+ field measurement_arr type array<int> {
+ indexing: input measurement | to_array | attribute
+ }
+
+ # Some experimental ranking changes
+ rank-profile experimental inherits default {
+ rank-type measurement: identity
+ }
+
+ rank-profile other inherits experimental {
+ rank-type measurement: identity
+ }
+
+ # check inheritance of phase expressions
+ rank-profile parent inherits default {
+ first-phase {
+ keep-rank-count:200
+ rank-score-drop-limit: -13.0
+ expression: attribute(year)
+ }
+ second-phase {
+ rerank-count: 99
+ }
+ macro openTicket() {
+ expression: if(attribute(status) == "accepted",1, if(attribute(status) == "new",1,if(attribute(status) == "reopened",1,0)))
+ }
+ }
+
+ rank-profile child inherits parent {
+ second-phase {
+ expression: attribute(access)
+ }
+ }
+
+ # A field defined outside an index
+ field exact type string {
+ indexing: input title . " " . input category | summary | index
+ summary-to: default
+ stemming: none
+ normalizing: none
+ rank-type: identity
+ }
+
+ field popsiness type int {
+ indexing: input popularity * input measurement | attribute
+ }
+}
diff --git a/config-model/src/test/examples/stemmingdefault.sd b/config-model/src/test/examples/stemmingdefault.sd
new file mode 100644
index 00000000000..f0ab6186f76
--- /dev/null
+++ b/config-model/src/test/examples/stemmingdefault.sd
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search stemmingdefault {
+ document stemmingdefault {
+ field my_str type string {
+
+ }
+ }
+}
diff --git a/config-model/src/test/examples/stemmingresolver.sd b/config-model/src/test/examples/stemmingresolver.sd
new file mode 100644
index 00000000000..097b24573ff
--- /dev/null
+++ b/config-model/src/test/examples/stemmingresolver.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search stemmingresolver {
+ document stemmingresolver {
+ field foo type string {
+ indexing: index
+ stemming: none
+ }
+ field bar type string {
+ indexing: index
+ }
+ }
+ fieldset default {
+ fields: foo, bar
+ }
+ stemming: all
+}
diff --git a/config-model/src/test/examples/stemmingsetting.sd b/config-model/src/test/examples/stemmingsetting.sd
new file mode 100644
index 00000000000..c75af3df435
--- /dev/null
+++ b/config-model/src/test/examples/stemmingsetting.sd
@@ -0,0 +1,36 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search stemmingsetting {
+
+ document stemmingsetting {
+
+ field artist type string {
+ indexing: index
+ stemming: shortest
+ }
+
+ field title type string {
+ indexing: index
+ stemming: none
+ }
+
+ field song type string {
+ indexing: index
+ stemming: multiple
+ }
+
+ field track type string {
+ # index-to: song, default
+ stemming: shortest
+ }
+
+ field backward type string {
+ indexing: index
+ stemming: all
+ }
+ }
+
+ index default {
+ stemming: shortest
+ }
+
+}
diff --git a/config-model/src/test/examples/strange.sd b/config-model/src/test/examples/strange.sd
new file mode 100644
index 00000000000..8f2b892b2ac
--- /dev/null
+++ b/config-model/src/test/examples/strange.sd
@@ -0,0 +1,24 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search strange {
+ document strange {
+ field source_src type string {
+
+ }
+ field idecidemyide type string {
+ id: 5
+ }
+ field sodoi type string {
+ id: 7
+ }
+ }
+ field source type string {
+ indexing {
+ input source_src | switch {
+ case "amg": input source_src | summary;
+ case "theweb": input source_src | summary | index;
+ default: input source_src . " partner" | summary | index;
+ };
+ }
+ stemming: none
+ }
+}
diff --git a/config-model/src/test/examples/struct.sd b/config-model/src/test/examples/struct.sd
new file mode 100755
index 00000000000..a4ada60e653
--- /dev/null
+++ b/config-model/src/test/examples/struct.sd
@@ -0,0 +1,17 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+ struct foo {
+ field bar type string { id: 1}
+ field fubar type int {}
+ }
+
+ struct bar {
+ field humbe type foo {}
+ }
+
+ field mystruct type foo {}
+ field arraystruct type array<foo> {}
+ field advanced type bar {}
+ }
+}
diff --git a/config-model/src/test/examples/struct_outside.sd b/config-model/src/test/examples/struct_outside.sd
new file mode 100644
index 00000000000..60a2a9344d5
--- /dev/null
+++ b/config-model/src/test/examples/struct_outside.sd
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search struct_outside {
+ struct my_struct {
+
+ }
+ document struct_outside {
+ field my_field type my_struct {
+
+ }
+ }
+}
diff --git a/config-model/src/test/examples/structanddocumentwithsamenames.sd b/config-model/src/test/examples/structanddocumentwithsamenames.sd
new file mode 100755
index 00000000000..572d451d451
--- /dev/null
+++ b/config-model/src/test/examples/structanddocumentwithsamenames.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search music {
+ document music {
+
+ field hello type music { }
+
+ struct music {
+ field banana type string { }
+ }
+
+ field foo type music { }
+
+ }
+}
diff --git a/config-model/src/test/examples/structoutsideofdocument.sd b/config-model/src/test/examples/structoutsideofdocument.sd
new file mode 100644
index 00000000000..a411e13f85c
--- /dev/null
+++ b/config-model/src/test/examples/structoutsideofdocument.sd
@@ -0,0 +1,16 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search structoutsideofdocument {
+
+ # (will fail)
+
+ struct nalle {
+ field foo type int {}
+ }
+
+ document {
+
+ field nallestruct type array<nalle> {
+ indexing: summary
+ body
+ }
+ }
diff --git a/config-model/src/test/examples/structresult.cfg b/config-model/src/test/examples/structresult.cfg
new file mode 100755
index 00000000000..addb5f6205c
--- /dev/null
+++ b/config-model/src/test/examples/structresult.cfg
@@ -0,0 +1,69 @@
+enablecompression false
+datatype[0].id 1381038251
+datatype[0].structtype[0].name "position"
+datatype[0].structtype[0].version 0
+datatype[0].structtype[0].compresstype NONE
+datatype[0].structtype[0].compresslevel 0
+datatype[0].structtype[0].compressthreshold 95
+datatype[0].structtype[0].compressminsize 800
+datatype[0].structtype[0].field[0].name "x"
+datatype[0].structtype[0].field[0].datatype 0
+datatype[0].structtype[0].field[1].name "y"
+datatype[0].structtype[0].field[1].datatype 0
+datatype[1].id 93505813
+datatype[1].structtype[0].name "bar"
+datatype[1].structtype[0].version 0
+datatype[1].structtype[0].compresstype NONE
+datatype[1].structtype[0].compresslevel 0
+datatype[1].structtype[0].compressthreshold 95
+datatype[1].structtype[0].compressminsize 800
+datatype[1].structtype[0].field[0].name "humbe"
+datatype[1].structtype[0].field[0].datatype 97614088
+datatype[2].id 97614088
+datatype[2].structtype[0].name "foo"
+datatype[2].structtype[0].version 0
+datatype[2].structtype[0].compresstype NONE
+datatype[2].structtype[0].compresslevel 0
+datatype[2].structtype[0].compressthreshold 95
+datatype[2].structtype[0].compressminsize 800
+datatype[2].structtype[0].field[0].name "fubar"
+datatype[2].structtype[0].field[0].datatype 0
+datatype[2].structtype[0].field[1].name "bar"
+datatype[2].structtype[0].field[1].id[0].id 1
+datatype[2].structtype[0].field[1].datatype 2
+datatype[3].id -1245205573
+datatype[3].arraytype[0].datatype 97614088
+datatype[4].id -1910204744
+datatype[4].structtype[0].name "music.header"
+datatype[4].structtype[0].version 0
+datatype[4].structtype[0].compresstype NONE
+datatype[4].structtype[0].compresslevel 0
+datatype[4].structtype[0].compressthreshold 95
+datatype[4].structtype[0].compressminsize 800
+datatype[4].structtype[0].field[0].name "mystruct"
+datatype[4].structtype[0].field[0].datatype 97614088
+datatype[4].structtype[0].field[1].name "arraystruct"
+datatype[4].structtype[0].field[1].datatype -1245205573
+datatype[4].structtype[0].field[2].name "advanced"
+datatype[4].structtype[0].field[2].datatype 93505813
+datatype[4].structtype[0].field[3].name "rankfeatures"
+datatype[4].structtype[0].field[3].datatype 2
+datatype[4].structtype[0].field[4].name "summaryfeatures"
+datatype[4].structtype[0].field[4].datatype 2
+datatype[5].id 993120973
+datatype[5].structtype[0].name "music.body"
+datatype[5].structtype[0].version 0
+datatype[5].structtype[0].compresstype NONE
+datatype[5].structtype[0].compresslevel 0
+datatype[5].structtype[0].compressthreshold 95
+datatype[5].structtype[0].compressminsize 800
+datatype[6].id 1412693671
+datatype[6].documenttype[0].name "music"
+datatype[6].documenttype[0].version 0
+datatype[6].documenttype[0].inherits[0].name "document"
+datatype[6].documenttype[0].inherits[0].version 0
+datatype[6].documenttype[0].headerstruct -1910204744
+datatype[6].documenttype[0].bodystruct 993120973
+datatype[6].documenttype[0].fieldsets{[document]}.fields[0] "advanced"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[1] "arraystruct"
+datatype[6].documenttype[0].fieldsets{[document]}.fields[2] "mystruct"
diff --git a/config-model/src/test/examples/summaryfieldcollision.sd b/config-model/src/test/examples/summaryfieldcollision.sd
new file mode 100644
index 00000000000..414afc1e6b6
--- /dev/null
+++ b/config-model/src/test/examples/summaryfieldcollision.sd
@@ -0,0 +1,26 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search summaryfieldcollision {
+
+ document summaryfieldcollision {
+
+ field title type string {
+ indexing: summary | index
+ }
+
+ field description type string {
+ indexing: summary | index
+ }
+ }
+
+ document-summary sum1 {
+ summary f type string {
+ source: title
+ }
+ }
+
+ document-summary sum2 {
+ summary f type string {
+ source: description
+ }
+ }
+}
diff --git a/config-model/src/test/examples/wrongending.expr b/config-model/src/test/examples/wrongending.expr
new file mode 100644
index 00000000000..745e8d376f7
--- /dev/null
+++ b/config-model/src/test/examples/wrongending.expr
@@ -0,0 +1 @@
+a + b
diff --git a/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java b/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java
new file mode 100644
index 00000000000..8958300538b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java
@@ -0,0 +1,379 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.google.common.io.Files;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.codegen.CNode;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.path.Path;
+import com.yahoo.document.DataType;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.io.IOUtils;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.UnproperSearch;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.search.SearchDefinition;
+import org.json.JSONException;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Pattern;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.Matchers.contains;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ApplicationDeployTest {
+
+ private static final String TESTDIR = "src/test/cfg/application/";
+ private static final String TESTSDDIR = TESTDIR + "app1/searchdefinitions/";
+
+ @Rule
+ public TemporaryFolder tmpFolder = new TemporaryFolder();
+
+ @Test
+ public void testVespaModel() throws SAXException, IOException {
+ FilesApplicationPackage app = createAppPkg(TESTDIR + "app1");
+ assertThat(app.getApplicationName(), is("app1"));
+ VespaModel model = new VespaModel(app);
+ List<SearchDefinition> searchDefinitions = getSearchDefinitions(app);
+ assertEquals(searchDefinitions.size(), 5);
+ for (SearchDefinition searchDefinition : searchDefinitions) {
+ Search s = searchDefinition.getSearch();
+ switch (s.getName()) {
+ case "music":
+ case "laptop":
+ case "pc":
+ case "sock":
+ break;
+ case "product":
+ assertTrue(s instanceof UnproperSearch);
+ assertEquals(s.getDocument().getField("title").getDataType(), DataType.STRING);
+ break;
+ default:
+ fail();
+ }
+ }
+ File[] truth = new File[]{new File(TESTSDDIR + "laptop.sd"),
+ new File(TESTSDDIR + "music.sd"),
+ new File(TESTSDDIR + "pc.sd"),
+ new File(TESTSDDIR + "product.sd"),
+ new File(TESTSDDIR + "sock.sd")};
+ Arrays.sort(truth);
+ List<File> appSdFiles = app.getSearchDefinitionFiles();
+ Collections.sort(appSdFiles);
+ assertEquals(appSdFiles, Arrays.asList(truth));
+
+ List<FilesApplicationPackage.Component> components = app.getComponents();
+ assertEquals(1, components.size());
+ Map<String, Bundle.DefEntry> defEntries =
+ defEntries2map(components.get(0).getDefEntries());
+ assertEquals(2, defEntries.size());
+ System.out.println(defEntries);
+ Bundle.DefEntry def1 = defEntries.get("test1");
+ Bundle.DefEntry def2 = defEntries.get("test2");
+ assertNotNull(def1);
+ assertNotNull(def2);
+ assertEquals("namespace=config\nintVal int default=0", def1.contents);
+ assertEquals("namespace=a.b\n\ndoubleVal double default=0.0", def2.contents);
+
+ // Check that getFilename works
+ ArrayList<String> sdFileNames = new ArrayList<>();
+ for (SearchDefinition sd : searchDefinitions) {
+ sdFileNames.add(sd.getFilename());
+ }
+ Collections.sort(sdFileNames);
+ assertThat(sdFileNames.get(0), is("laptop.sd"));
+ assertThat(sdFileNames.get(1), is("music.sd"));
+ assertThat(sdFileNames.get(2), is("pc.sd"));
+ assertThat(sdFileNames.get(3), is("product.sd"));
+ assertThat(sdFileNames.get(4), is("sock.sd"));
+ }
+
+ @Test
+ public void testGetFile() throws IOException {
+ FilesApplicationPackage app = createAppPkg(TESTDIR + "app1");
+ try (Reader foo = app.getFile(Path.fromString("files/foo.json")).createReader()) {
+ assertEquals(IOUtils.readAll(foo), "foo : foo\n");
+ }
+ try (Reader bar = app.getFile(Path.fromString("files/sub/bar.json")).createReader()) {
+ assertEquals(IOUtils.readAll(bar), "bar : bar\n");
+ }
+ assertTrue(app.getFile(Path.createRoot()).exists());
+ assertTrue(app.getFile(Path.createRoot()).isDirectory());
+ }
+
+ /*
+ * Put a list of def entries to a map, with the name as key. This is done because the order
+ * of the def entries in the list cannot be guaranteed.
+ */
+ private Map<String, Bundle.DefEntry> defEntries2map
+ (List<Bundle.DefEntry> defEntries) {
+ Map<String, Bundle.DefEntry> ret =
+ new HashMap<>();
+
+ for (Bundle.DefEntry def : defEntries)
+ ret.put(def.defName, def);
+ return ret;
+ }
+
+ @Test
+ public void testSdFromDocprocBundle() throws IOException, SAXException {
+ String appDir = "src/test/cfg/application/app_sdbundles";
+ FilesApplicationPackage app = createAppPkg(appDir);
+ VespaModel model = new VespaModel(app);
+ // Check that the resulting documentmanager config contains those types
+ DocumentmanagerConfig.Builder b = new DocumentmanagerConfig.Builder();
+ model.getConfig(b, VespaModel.ROOT_CONFIGID);
+ //String docMan = model.getConfig("documentmanager", "").toString();
+ DocumentmanagerConfig dc = new DocumentmanagerConfig(b);
+ String docMan=ConfigInstance.serialize(dc).toString();
+ int pFlags = Pattern.MULTILINE + Pattern.DOTALL;
+ Pattern base = Pattern.compile(".*name.*base\\.header.*", pFlags);
+ Pattern book = Pattern.compile(".*name.*book\\.header.*", pFlags);
+ Pattern music = Pattern.compile(".*name.*music\\.header.*", pFlags);
+ Pattern video = Pattern.compile(".*name.*video\\.header.*", pFlags);
+ Pattern muzak = Pattern.compile(".*name.*muzak\\.header.*", pFlags);
+ assertTrue(base.matcher(docMan).matches());
+ assertTrue(book.matcher(docMan).matches());
+ assertTrue(music.matcher(docMan).matches());
+ assertTrue(video.matcher(docMan).matches());
+ assertTrue(muzak.matcher(docMan).matches());
+ }
+
+ @Test
+ public void include_dirs_are_included() throws Exception {
+ FilesApplicationPackage app = createAppPkg(TESTDIR + "include_dirs");
+
+ List<String> includeDirs = app.getUserIncludeDirs();
+ assertThat(includeDirs, contains("jdisc_dir", "dir1", "dir2", "empty_dir"));
+ }
+
+ @Test
+ public void non_existent_include_dir_is_not_allowed() throws Exception {
+ File appDir = tmpFolder.newFolder("non-existent-include");
+ String services = "<services version='1.0'>" +
+ "<include dir='non-existent' />" +
+ "</services>\n";
+
+ IOUtils.writeFile(new File(appDir, "services.xml"), services, false);
+ try {
+ FilesApplicationPackage.fromFile(appDir);
+ fail("Expected exception due to non-existent include dir");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("Cannot include directory 'non-existent', as it does not exist"));
+ }
+ }
+
+ @Test
+ public void testThatModelIsRebuiltWhenSearchDefinitionIsAdded() throws IOException {
+ File tmpDir = Files.createTempDir();
+ IOUtils.copyDirectory(new File(TESTDIR, "app1"), tmpDir);
+ FilesApplicationPackage app = createAppPkg(tmpDir.getAbsolutePath());
+ assertThat(getSearchDefinitions(app).size(), is(5));
+ File sdDir = new File(tmpDir, "searchdefinitions");
+ File sd = new File(sdDir, "testfoo.sd");
+ IOUtils.writeFile(sd, "search testfoo { document testfoo { field bar type string { } } }", false);
+ assertThat(getSearchDefinitions(app).size(), is(6));
+ }
+
+ private List<SearchDefinition> getSearchDefinitions(FilesApplicationPackage app) {
+ return new DeployState.Builder().applicationPackage(app).build().getSearchDefinitions();
+ }
+
+ public FilesApplicationPackage createAppPkg(String appPkg) throws IOException {
+ return createAppPkg(appPkg, true);
+ }
+
+ public FilesApplicationPackage createAppPkgDoNotValidateXml(String appPkg) throws IOException {
+ return createAppPkg(appPkg, false);
+ }
+
+ public FilesApplicationPackage createAppPkg(String appPkg, boolean validateXml) throws IOException {
+ final FilesApplicationPackage filesApplicationPackage = FilesApplicationPackage.fromFile(new File(appPkg));
+ if (validateXml) {
+ ApplicationPackageXmlFilesValidator validator = ApplicationPackageXmlFilesValidator.createTestXmlValidator(new File(appPkg));
+ validator.checkApplication();
+ ApplicationPackageXmlFilesValidator.checkIncludedDirs(filesApplicationPackage);
+ }
+ return filesApplicationPackage;
+ }
+
+ @Test
+ public void testThatNewServicesFileNameWorks() throws IOException {
+ String appPkg = TESTDIR + "newfilenames";
+ assertEquals(appPkg + "/services.xml", createAppPkgDoNotValidateXml(appPkg).getServicesSource());
+ }
+
+ @Test
+ public void testThatNewHostsFileNameWorks() throws IOException {
+ String appPkg = TESTDIR + "newfilenames";
+ assertEquals(appPkg + "/hosts.xml", createAppPkgDoNotValidateXml(appPkg).getHostSource());
+ }
+
+ @Test
+ public void testGetJars() throws IOException {
+ String jarName = "src/test/cfg/application/app_sdbundles/components/testbundle.jar";
+ JarFile jar = new JarFile(jarName);
+ Map<String, String> payloads = ApplicationPackage.getBundleSdFiles("", jar);
+ assertEquals(payloads.size(), 4);
+ assertTrue(payloads.get("base.sd").startsWith("search base"));
+ assertTrue(payloads.get("book.sd").startsWith("search book"));
+ assertTrue(payloads.get("music.sd").startsWith("search music"));
+ assertTrue(payloads.get("video.sd").startsWith("search video"));
+ assertTrue(payloads.get("base.sd").endsWith("}"));
+ assertTrue(payloads.get("book.sd").endsWith("}\n"));
+ assertTrue(payloads.get("music.sd").endsWith("}\n"));
+ assertTrue(payloads.get("video.sd").endsWith("}\n"));
+ }
+
+ @Test
+ public void testConfigDefinitionsFromJars() throws IOException {
+ String appName = "src/test/cfg//application/app1";
+ FilesApplicationPackage app = FilesApplicationPackage.fromFile(new File(appName));
+ Map<ConfigDefinitionKey, UnparsedConfigDefinition> defs = app.getAllExistingConfigDefs();
+ assertThat(defs.size(), is(2));
+ }
+
+ @Test
+ public void testMetaData() throws IOException, JSONException {
+ File tmp = Files.createTempDir();
+ String appPkg = TESTDIR + "app1";
+ IOUtils.copyDirectory(new File(appPkg), tmp);
+ final DeployData deployData = new DeployData("foo", "bar", "baz", 13l, 1337l, 3l);
+ FilesApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData);
+ app.writeMetaData();
+ FilesApplicationPackage newApp = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData);
+ ApplicationMetaData meta = newApp.getMetaData();
+ assertThat(meta.getDeployedByUser(), is("foo"));
+ assertThat(meta.getDeployPath(), is("bar"));
+ assertThat(meta.getDeployTimestamp(), is(13l));
+ assertThat(meta.getGeneration(), is(1337l));
+ assertThat(meta.getPreviousActiveGeneration(), is(3l));
+ final String checkSum = meta.getCheckSum();
+ assertNotNull(checkSum);
+
+ assertTrue((new File(tmp, "hosts.xml")).delete());
+ FilesApplicationPackage app2 = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData);
+ final String app2CheckSum = app2.getMetaData().getCheckSum();
+ assertThat(app2CheckSum, is(not(checkSum)));
+
+ assertTrue((new File(tmp, "files/foo.json")).delete());
+ FilesApplicationPackage app3 = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData);
+ final String app3CheckSum = app3.getMetaData().getCheckSum();
+ assertThat(app3CheckSum, is(not(app2CheckSum)));
+ }
+
+ @Test
+ public void testGetJarEntryName() {
+ JarEntry e = new JarEntry("/searchdefinitions/foo.sd");
+ assertEquals(ApplicationPackage.getFileName(e), "foo.sd");
+ e = new JarEntry("bar");
+ assertEquals(ApplicationPackage.getFileName(e), "bar");
+ e = new JarEntry("");
+ assertEquals(ApplicationPackage.getFileName(e), "");
+ }
+
+ @After
+ public void cleanDirs() {
+ IOUtils.recursiveDeleteDir(new File(TESTDIR + "app1/myDir"));
+ IOUtils.recursiveDeleteDir(new File(TESTDIR + "app1/searchdefinitions/myDir2"));
+ IOUtils.recursiveDeleteDir(new File(TESTDIR + "app1/myDir3"));
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ @After
+ public void cleanFiles() {
+ new File(new File(TESTDIR + "app1"),"foo.txt").delete();
+ new File(new File(TESTDIR + "app1"),"searchdefinitions/bar.text").delete();
+ IOUtils.recursiveDeleteDir(new File(TESTDIR + "app1/mySubDir"));
+ }
+
+ /**
+ * Tests that an invalid jar is identified as not being a jar file
+ */
+ @Test
+ public void testInvalidJar() {
+ try {
+ FilesApplicationPackage.getComponents(new File("src/test/cfg/application/validation/invalidjar_app"));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("Error opening jar file 'invalid.jar'. Please check that this is a valid jar file"));
+ }
+ }
+
+ /**
+ * Tests that config definitions with namespace are treated properly when they have the format
+ * as in the config definitions dir ($VESPA_HOME/var/db/vespa/config_server/serverdb/classes on a machine
+ * with Vespa packages installed) (does not test when read from user def files). Also tests a config
+ * definition without version in file name
+ */
+ @Test
+ public void testConfigDefinitionsAndNamespaces() {
+ final File appDir = new File("src/test/cfg/application/configdeftest");
+ FilesApplicationPackage app = FilesApplicationPackage.fromFile(appDir);
+
+ DeployState deployState = new DeployState.Builder().applicationPackage(app).build();
+
+ ConfigDefinition def = deployState.getConfigDefinition(new ConfigDefinitionKey("foo", CNode.DEFAULT_NAMESPACE));
+ assertThat(def.getNamespace(), is(CNode.DEFAULT_NAMESPACE));
+
+ def = deployState.getConfigDefinition(new ConfigDefinitionKey("baz", CNode.DEFAULT_NAMESPACE));
+ assertThat(def.getNamespace(), is("xyzzy"));
+
+ def = deployState.getConfigDefinition(new ConfigDefinitionKey("foo", "qux"));
+ assertThat(def.getNamespace(), is("qux"));
+
+ // A config def without version in filename and version in file header
+ def = deployState.getConfigDefinition(new ConfigDefinitionKey("xyzzy", CNode.DEFAULT_NAMESPACE));
+ assertThat(def.getNamespace(), is(CNode.DEFAULT_NAMESPACE));
+ assertThat(def.getName(), is("xyzzy"));
+
+ // Without giving namespace, namespace is really CNode.DEFAULT_NAMESPACE
+ def = deployState.getConfigDefinition(new ConfigDefinitionKey("baz", ""));
+ assertThat(def.getNamespace(), is("xyzzy"));
+
+ // Without giving namespace, namespace is really xyzzy
+ def = deployState.getConfigDefinition(new ConfigDefinitionKey("baz", ""));
+ assertThat(def.getNamespace(), is("xyzzy"));
+
+ // Two defs, one with and one without namespace. The one with namespace should have precedence.
+ def = deployState.getConfigDefinition(new ConfigDefinitionKey("bar", "xyzzy"));
+ assertThat(def.getNamespace(), is("xyzzy"));
+ assertTrue(def.getIntDefs().containsKey("foo")); // xyzzy.baz.def has precedence before baz.def, so foo exists
+ assertThat(def.getIntDefs().get("bar").getDefVal(), is(2));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ public void testDifferentNameOfSdFileAndSearchName() throws SAXException, IOException {
+ FilesApplicationPackage app = createAppPkg(TESTDIR + "sdfilenametest");
+ new DeployState.Builder().applicationPackage(app).build();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/ConfigModelBuilderTest.java b/config-model/src/test/java/com/yahoo/config/model/ConfigModelBuilderTest.java
new file mode 100644
index 00000000000..10efd62479d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/ConfigModelBuilderTest.java
@@ -0,0 +1,121 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigModelBuilderTest {
+ @Test
+ public void testEquals() {
+ ConfigModelBuilder<?> ba = new A.Builder();
+ ConfigModelBuilder<?> ba2 = new A2.Builder();
+ ConfigModelBuilder<?> bb = new B.Builder();
+ ConfigModelBuilder <?>bb2 = new B2.Builder();
+
+ assertTrue(ba.equals(ba));
+ assertTrue(ba.equals(ba2));
+ assertFalse(ba.equals(bb));
+ assertFalse(ba.equals(bb2));
+
+ assertTrue(ba2.equals(ba));
+ assertTrue(ba2.equals(ba2));
+ assertFalse(ba2.equals(bb));
+ assertFalse(ba2.equals(bb2));
+
+ assertFalse(bb.equals(ba));
+ assertFalse(bb.equals(ba2));
+ assertTrue(bb.equals(bb));
+ assertFalse(bb.equals(bb2));
+
+ assertFalse(bb2.equals(ba));
+ assertFalse(bb2.equals(ba2));
+ assertFalse(bb2.equals(bb));
+ assertTrue(bb2.equals(bb2));
+
+ assertFalse(ba.equals(new ArrayList<>()));
+ }
+
+ private static class A extends ConfigModel {
+ public A(ConfigModelContext modelContext) { super(modelContext); }
+ public static class Builder extends ConfigModelBuilder<A> {
+ public Builder() { super(A.class); }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ List<ConfigModelId> ids = new ArrayList<>();
+ ids.add(ConfigModelId.fromName("foo"));
+ ids.add(ConfigModelId.fromName("bar"));
+ return ids;
+ }
+
+ @Override
+ public void doBuild(A model, Element spec, ConfigModelContext modelContext) { }
+ }
+
+ }
+
+ private static class A2 extends ConfigModel {
+ public A2(ConfigModelContext modelContext) { super(modelContext); }
+ public static class Builder extends ConfigModelBuilder<A2> {
+ public Builder() { super(A2.class); }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ List<ConfigModelId> ids = new ArrayList<>();
+ ids.add(ConfigModelId.fromName("foo"));
+ ids.add(ConfigModelId.fromName("bar"));
+ return ids;
+ }
+
+ @Override
+ public void doBuild(A2 model, Element spec, ConfigModelContext modelContext) { }
+ }
+ }
+
+ private static class B extends ConfigModel {
+ public B(ConfigModelContext modelContext) { super(modelContext); }
+ public static class Builder extends ConfigModelBuilder<B> {
+ public Builder() { super(B.class); }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ List<ConfigModelId> ids = new ArrayList<>();
+ ids.add(ConfigModelId.fromName("bar"));
+ return ids;
+ }
+
+ @Override
+ public void doBuild(B model, Element spec, ConfigModelContext modelContext) { }
+ }
+ }
+
+ private static class B2 extends ConfigModel {
+ public B2(ConfigModelContext modelContext) { super(modelContext); }
+ public static class Builder extends ConfigModelBuilder<B2> {
+ public Builder() { super(B2.class); }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ List<ConfigModelId> ids = new ArrayList<>();
+ ids.add(ConfigModelId.fromName("foo"));
+ ids.add(ConfigModelId.fromName("bim"));
+ return ids;
+ }
+
+ @Override
+ public void doBuild(B2 model, Element spec, ConfigModelContext modelContext) { }
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/ConfigModelContextTest.java b/config-model/src/test/java/com/yahoo/config/model/ConfigModelContextTest.java
new file mode 100644
index 00000000000..4deb8d427c4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/ConfigModelContextTest.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.MockRoot;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigModelContextTest {
+ @Test
+ public void testConfigModelContext() {
+ AbstractConfigProducer root = new MockRoot();
+ String id = "foobar";
+ ApplicationPackage pkg = new MockApplicationPackage.Builder()
+ .withServices("<services version=\"1.0\"><admin version=\"2.0\" /></services>")
+ .build();
+ DeployState deployState = DeployState.createTestState(pkg);
+ DeployLogger logger = deployState.getDeployLogger();
+ ConfigModelContext ctx = ConfigModelContext.create(deployState, null, root, id);
+ assertThat(ctx.getApplicationPackage(), is(pkg));
+ assertThat(ctx.getProducerId(), is(id));
+ assertThat(ctx.getParentProducer(), is(root));
+ assertThat(ctx.getDeployLogger(), is(logger));
+ ctx = ConfigModelContext.createFromParentAndId(null, root, id);
+ assertThat(ctx.getProducerId(), is(id));
+ assertThat(ctx.getParentProducer(), is(root));
+ AbstractConfigProducer newRoot = new MockRoot("bar");
+ ctx = ctx.modifyParent(newRoot);
+ assertThat(ctx.getProducerId(), is(id));
+ assertThat(ctx.getParentProducer(), is(not(root)));
+ assertThat(ctx.getParentProducer(), is(newRoot));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/ConfigModelUtilsTest.java b/config-model/src/test/java/com/yahoo/config/model/ConfigModelUtilsTest.java
new file mode 100644
index 00000000000..1630c317825
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/ConfigModelUtilsTest.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.model.application.provider.Bundle;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+
+import static junit.framework.TestCase.fail;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigModelUtilsTest {
+
+ /**
+ * Tests that a def file both with and without namespace in file name are handled, and that
+ * def files in other directories than 'configdefinitions/' within the jar file are ignored.
+ */
+ @Test
+ public void testDefFilesInBundle() {
+ List<Bundle> bundles = Bundle.getBundles(new File("src/test/cfg/application/app1/components/"));
+ assertThat(bundles.size(), is(1));
+ Bundle bundle = bundles.get(0);
+ assertThat(bundle.getDefEntries().size(), is(2));
+
+ Bundle.DefEntry defEntry1 = bundle.getDefEntries().get(0);
+ Bundle.DefEntry defEntry2;
+ List<Bundle.DefEntry> defEntries = bundle.getDefEntries();
+ if (defEntry1.defName.equals("test1")) {
+ defEntry2 = defEntries.get(1);
+ } else {
+ defEntry1 = defEntries.get(1);
+ defEntry2 = defEntries.get(0);
+ }
+ assertThat(defEntry1.defName, is("test1"));
+ assertThat(defEntry1.defNamespace, is("config"));
+
+ assertThat(defEntry2.defName, is("test2"));
+ assertThat(defEntry2.defNamespace, is("a.b"));
+ }
+
+ /**
+ * Tests that an invalid jar is identified as not being a jar file
+ */
+ @Test
+ public void testInvalidJar() {
+ try {
+ Bundle.getBundles(new File("src/test/cfg/application/validation/invalidjar_app/components"));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("Error opening jar file 'invalid.jar'. Please check that this is a valid jar file"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/MapConfigModelRegistryTest.java b/config-model/src/test/java/com/yahoo/config/model/MapConfigModelRegistryTest.java
new file mode 100644
index 00000000000..ea32feff8a9
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/MapConfigModelRegistryTest.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class MapConfigModelRegistryTest {
+
+ @Test
+ public void require_that_registry_finds_components() {
+ ModelABuilder ba = new ModelABuilder();
+ ModelBBuilder bb = new ModelBBuilder();
+ ConfigModelRegistry registry = MapConfigModelRegistry.createFromList(ba, bb);
+ assertNotNull(registry.resolve(ConfigModelId.fromName("modelA")));
+ assertNotNull(registry.resolve(ConfigModelId.fromName("modelB")));
+ assertEquals(ba, registry.resolve(ConfigModelId.fromName("modelA")).iterator().next());
+ assertEquals(bb, registry.resolve(ConfigModelId.fromName("modelB")).iterator().next());
+ assertTrue(registry.resolve(ConfigModelId.fromName("modelC")).isEmpty());
+ }
+
+ @Test
+ public void require_all_builders_for_a_tag() {
+ ModelBBuilder b1 = new ModelBBuilder();
+ ModelB2Builder b2 = new ModelB2Builder();
+ ConfigModelRegistry registry = MapConfigModelRegistry.createFromList(b1, b2);
+ Collection<ConfigModelBuilder> builders = registry.resolve(ConfigModelId.fromName("modelB"));
+ assertEquals(2, builders.size());
+ assertEquals(b1, builders.iterator().next());
+ assertEquals(b2, builders.iterator().next());
+ }
+
+ private static class ModelB2Builder extends ConfigModelBuilder<ModelB> {
+ public ModelB2Builder() {
+ super(ModelB.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() { return Arrays.asList(ConfigModelId.fromName("modelB")); }
+ @Override
+ public void doBuild(ModelB model, Element spec, ConfigModelContext modelContext) { }
+ }
+
+ private static class ModelBBuilder extends ConfigModelBuilder<ModelB> {
+ public ModelBBuilder() {
+ super(ModelB.class);
+ }
+ @Override
+ public List<ConfigModelId> handlesElements() { return Arrays.asList(ConfigModelId.fromName("modelB")); }
+ @Override
+ public void doBuild(ModelB model, Element spec, ConfigModelContext modelContext) { }
+ }
+
+ private class ModelB extends ConfigModel {
+ protected ModelB(ConfigModelContext modelContext) {
+ super(modelContext);
+ }
+ }
+
+ private static class ModelABuilder extends ConfigModelBuilder<ModelA> {
+ public ModelABuilder() {
+ super(ModelA.class);
+ }
+ @Override
+ public List<ConfigModelId> handlesElements() { return Arrays.asList(ConfigModelId.fromName("modelA")); }
+
+ @Override
+ public void doBuild(ModelA model, Element spec, ConfigModelContext modelContext) { }
+ }
+
+ private class ModelA extends ConfigModel {
+ protected ModelA(ConfigModelContext modelContext) {
+ super(modelContext);
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/MockModelContext.java b/config-model/src/test/java/com/yahoo/config/model/MockModelContext.java
new file mode 100644
index 00000000000..c462cde3fb2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/MockModelContext.java
@@ -0,0 +1,103 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.config.model.api.*;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.FileRegistry;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.application.provider.StaticConfigDefinitionRepo;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.config.provision.Zone;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+* @author musum
+*/
+public class MockModelContext implements ModelContext {
+ private final ApplicationPackage applicationPackage;
+
+ public MockModelContext() {
+ this.applicationPackage = MockApplicationPackage.createEmpty();
+ }
+
+ public MockModelContext(ApplicationPackage applicationPackage) {
+ this.applicationPackage = applicationPackage;
+ }
+
+ @Override
+ public ApplicationPackage applicationPackage() {
+ return applicationPackage;
+ }
+
+ @Override
+ public Optional<Model> previousModel() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<ApplicationPackage> permanentApplicationPackage() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<HostProvisioner> hostProvisioner() {
+ return Optional.empty();
+ }
+
+ @Override
+ public DeployLogger deployLogger() {
+ return new BaseDeployLogger();
+ }
+
+ @Override
+ public ConfigDefinitionRepo configDefinitionRepo() {
+ return new StaticConfigDefinitionRepo();
+ }
+
+ @Override
+ public FileRegistry getFileRegistry() {
+ return new MockFileRegistry();
+ }
+
+ @Override
+ public Properties properties() {
+ return new Properties() {
+ @Override
+ public boolean multitenant() {
+ return false;
+ }
+
+ @Override
+ public ApplicationId applicationId() {
+ return ApplicationId.defaultId();
+ }
+
+ @Override
+ public List<ConfigServerSpec> configServerSpecs() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean hostedVespa() {return false; }
+
+ @Override
+ public Zone zone() {
+ return Zone.defaultZone();
+ }
+
+ @Override
+ public Set<Rotation> rotations() {
+ return new HashSet<>();
+ }
+ };
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/QrserverAndGatewayPortAllocationTest.java b/config-model/src/test/java/com/yahoo/config/model/QrserverAndGatewayPortAllocationTest.java
new file mode 100644
index 00000000000..28a2a163b66
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/QrserverAndGatewayPortAllocationTest.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model;
+
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests that qrserver is assigned port Defaults.getDefaults().vespaWebServicePort() even if there is a HTTP gateway configured earlier in
+ * vespa-services.xml
+ *
+ * @author musum
+ */
+public class QrserverAndGatewayPortAllocationTest {
+
+ @Test
+ public void testPorts() throws IOException, SAXException {
+ String appDir = "src/test/cfg/application/app_qrserverandgw/";
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg(appDir);
+ VespaModel vespaModel = creator.create();
+ List<Container> qrservers = vespaModel.getContainerClusters().get("container").getContainers();
+ assertThat(qrservers.size(), is(1));
+ assertThat(qrservers.get(0).getSearchPort(), is(Container.BASEPORT));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/application/provider/SchemaValidatorTest.java b/config-model/src/test/java/com/yahoo/config/model/application/provider/SchemaValidatorTest.java
new file mode 100644
index 00000000000..63b51acfad5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/application/provider/SchemaValidatorTest.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.application.provider;
+
+import org.junit.Test;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ * @since 5.1.9
+ */
+public class SchemaValidatorTest {
+
+ private static final String okServices = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services>" +
+ " <config name=\"standard\">" +
+ " <basicStruct>" +
+ " <stringVal>default</stringVal>" +
+ " </basicStruct>" +
+ " </config> " +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node1\" />" +
+ " </admin>" +
+ "</services>";
+
+ private static final String badServices = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services>" +
+ " <config name=\"standard\">" +
+ " <basicStruct>" +
+ " <stringVal>default</stringVal>" +
+ " </basicStruct>" +
+ " </config> " +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node1\"" +
+ " </admin>" +
+ "</services>";
+
+
+ @Test
+ public void testXMLParse() throws SAXException, IOException {
+ SchemaValidator validator = createValidator();
+ validator.validate(new InputSource(new StringReader(okServices)), "services.xml");
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testXMLParseError() throws SAXException, IOException {
+ SchemaValidator validator = createValidator();
+ validator.validate(new InputSource(new StringReader(badServices)), "services.xml");
+ }
+
+ @Test
+ public void testXMLParseWithReader() throws SAXException, IOException {
+ SchemaValidator validator = createValidator();
+ validator.validate(new StringReader(okServices));
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testXMLParseErrorWithReader() throws SAXException, IOException {
+ SchemaValidator validator = createValidator();
+ validator.validate(new StringReader(badServices));
+ }
+
+ private SchemaValidator createValidator() throws IOException {
+ return SchemaValidator.createTestValidatorServices();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/builder/xml/ConfigModelIdTest.java b/config-model/src/test/java/com/yahoo/config/model/builder/xml/ConfigModelIdTest.java
new file mode 100644
index 00000000000..e3baa3c1796
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/builder/xml/ConfigModelIdTest.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.builder.xml;
+
+import com.yahoo.component.Version;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ConfigModelIdTest {
+
+ @Test
+ public void require_that_element_gets_correct_name() {
+ ConfigModelId id = ConfigModelId.fromName("foo");
+ assertThat(id.getName(), is("foo"));
+ assertThat(id.getVersion(), is(Version.fromString("1")));
+ id = ConfigModelId.fromNameAndVersion("bar", "2.2");
+ assertThat(id.getName(), is("bar"));
+ assertThat(id.getVersion(), is(Version.fromString("2.2")));
+ }
+
+ @Test
+ public void test_toString() {
+ ConfigModelId id = ConfigModelId.fromNameAndVersion("bar", "1.0");
+ assertThat(id.toString(), is("bar.1"));
+ id = ConfigModelId.fromNameAndVersion("foo", "1.1.3");
+ assertThat(id.toString(), is("foo.1.1.3"));
+ id = ConfigModelId.fromNameAndVersion("bar", "1");
+ assertThat(id.toString(), is("bar.1"));
+ }
+
+ @Test
+ public void test_equality() {
+ ConfigModelId a1 = ConfigModelId.fromName("a");
+ ConfigModelId a2 = ConfigModelId.fromName("a");
+ ConfigModelId b = ConfigModelId.fromName("b");
+ assertTrue(a1.equals(a2));
+ assertTrue(a2.equals(a1));
+ assertFalse(a1.equals(b));
+ assertFalse(a2.equals(b));
+ assertFalse(b.equals(a1));
+ assertFalse(b.equals(a2));
+ assertTrue(a1.equals(a1));
+ assertTrue(a2.equals(a2));
+ assertTrue(b.equals(b));
+ }
+
+ @Test
+ public void test_compare() {
+ ConfigModelId a1 = ConfigModelId.fromName("a");
+ ConfigModelId a2 = ConfigModelId.fromName("a");
+ ConfigModelId b = ConfigModelId.fromName("b");
+ assertTrue(a1.compareTo(a2) == 0);
+ assertTrue(a2.compareTo(a1) == 0);
+ assertFalse(a1.compareTo(b) > 0);
+ assertFalse(a2.compareTo(b) > 0);
+ assertFalse(b.compareTo(a1) < 0);
+ assertFalse(b.compareTo(a2) < 0);
+ assertTrue(a1.compareTo(a1) == 0);
+ assertTrue(a2.compareTo(a2) == 0);
+ assertTrue(b.compareTo(b) == 0);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/builder/xml/test/DomBuilderTest.java b/config-model/src/test/java/com/yahoo/config/model/builder/xml/test/DomBuilderTest.java
new file mode 100644
index 00000000000..ba5e3e4d816
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/builder/xml/test/DomBuilderTest.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.builder.xml.test;
+
+import com.yahoo.config.model.test.ConfigModelTestUtil;
+import com.yahoo.config.model.test.MockRoot;
+import org.junit.Before;
+import org.w3c.dom.Element;
+
+/**
+ * Utility functions for testing dom builders.
+ *
+ * For an example,
+ * @see com.yahoo.vespa.model.builder.xml.dom.chains.DependenciesBuilderTest
+ *
+ * @author tonytv
+ */
+abstract public class DomBuilderTest {
+
+ public static Element parse(String... xmlLines) {
+ return ConfigModelTestUtil.parse(xmlLines);
+ }
+
+ protected MockRoot root;
+
+ @Before
+ public void setup() {
+ root = new MockRoot();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/deploy/DeployStateTest.java b/config-model/src/test/java/com/yahoo/config/model/deploy/DeployStateTest.java
new file mode 100644
index 00000000000..a84cb7ca7f6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/deploy/DeployStateTest.java
@@ -0,0 +1,148 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.deploy;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.codegen.InnerCNode;
+import com.yahoo.config.model.api.ConfigDefinitionRepo;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Rotation;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author lulf
+ * @since 5.12
+ */
+public class DeployStateTest {
+ @Test
+ public void testProvisionerIsSet() {
+ DeployState.Builder builder = new DeployState.Builder();
+ HostProvisioner provisioner = new InMemoryProvisioner(true, "foo.yahoo.com");
+ builder.modelHostProvisioner(provisioner);
+ DeployState state = builder.build();
+ assertThat(state.getProvisioner(), is(provisioner));
+ }
+
+ @Test
+ public void testBuilder() {
+ DeployState.Builder builder = new DeployState.Builder();
+ ApplicationPackage app = MockApplicationPackage.createEmpty();
+ builder.permanentApplicationPackage(Optional.of(app));
+ DeployState state = builder.build();
+ assertThat(state.getPermanentApplicationPackage().get(), is(app));
+ }
+
+ @Test
+ public void testPreviousModelIsProvided() throws IOException, SAXException {
+ VespaModel prevModel = new VespaModel(MockApplicationPackage.createEmpty());
+ DeployState.Builder builder = new DeployState.Builder();
+ assertThat(builder.previousModel(prevModel).build().getPreviousModel().get(), is(prevModel));
+ }
+
+ @Test
+ public void testProperties() {
+ DeployState.Builder builder = new DeployState.Builder();
+ DeployState state = builder.build();
+ assertThat(state.getProperties().applicationId(), is(ApplicationId.defaultId()));
+ ApplicationId customId = new ApplicationId.Builder()
+ .tenant("bar")
+ .applicationName("foo").instanceName("quux").build();
+ DeployProperties properties = new DeployProperties.Builder().applicationId(customId).build();
+ builder.properties(properties);
+ state = builder.build();
+ assertThat(state.getProperties().applicationId(), is(customId));
+ }
+
+ @Test
+ public void testDefinitionRepoIsUsed() {
+ final Map<ConfigDefinitionKey, com.yahoo.vespa.config.buildergen.ConfigDefinition> defs = new LinkedHashMap<>();
+ defs.put(new ConfigDefinitionKey("foo", "bar"), new com.yahoo.vespa.config.buildergen.ConfigDefinition("foo", new String[]{"namespace=bar", "foo int default=0"}));
+ defs.put(new ConfigDefinitionKey("test2", "a.b"),
+ new com.yahoo.vespa.config.buildergen.ConfigDefinition("test2", new String[]{"namespace=a.b", "doubleVal double default=1.0"}));
+ ApplicationPackage app = FilesApplicationPackage.fromFile(new File("src/test/cfg//application/app1"));
+ DeployState state = createDeployState(app, defs);
+
+ assertNotNull(state.getConfigDefinition(new ConfigDefinitionKey("foo", "bar")));
+ assertNotNull(state.getConfigDefinition(new ConfigDefinitionKey("test1", "")));
+ ConfigDefinition overridden = state.getConfigDefinition(new ConfigDefinitionKey("test2", "a.b"));
+ assertNotNull(overridden);
+ Double defaultValue = overridden.getDoubleDefs().get("doubleVal").getDefVal();
+ assertNotNull(defaultValue);
+ assertThat(defaultValue.intValue(), is(0));
+ }
+
+ @Test
+ public void testGetConfigDefinition() {
+ final Map<ConfigDefinitionKey, com.yahoo.vespa.config.buildergen.ConfigDefinition> defs = new LinkedHashMap<>();
+ defs.put(new ConfigDefinitionKey("test2", "a.b"), new com.yahoo.vespa.config.buildergen.ConfigDefinition("test2", new String[]{"namespace=a.b", "doubleVal double default=1.0"}));
+ defs.put(new ConfigDefinitionKey("test2", "c.d"), new com.yahoo.vespa.config.buildergen.ConfigDefinition("test2", new String[]{"namespace=c.d", "doubleVal double default=1.0"}));
+ defs.put(new ConfigDefinitionKey("test3", "xyzzy"), new com.yahoo.vespa.config.buildergen.ConfigDefinition("test3", new String[]{"namespace=xyzzy", "message string"}));
+ ApplicationPackage app = FilesApplicationPackage.fromFile(new File("src/test/cfg//application/app1"));
+ DeployState state = createDeployState(app, defs);
+
+ assertNotNull(state.getConfigDefinition(new ConfigDefinitionKey("test2", "a.b")));
+
+ // Should not fallback to using test2 with another namespace
+ try {
+ state.getConfigDefinition(new ConfigDefinitionKey("test2", ""));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("Using config definition 'test2' is ambiguous, there are more than one config definitions with this name, please specify namespace"));
+ }
+
+ final ConfigDefinition test1 = state.getConfigDefinition(new ConfigDefinitionKey("test2", "a.b"));
+ assertNotNull(test1);
+ assertThat(test1.getName(), is("test2"));
+ assertThat(test1.getNamespace(), is("a.b"));
+
+ // Should fallback to using test3 with another namespace, since only one exists
+ ConfigDefinition test3 = state.getConfigDefinition(new ConfigDefinitionKey("test3", ""));
+ assertNotNull(test3);
+ assertThat(test3.getName(), is("test3"));
+ assertThat(test3.getNamespace(), is("xyzzy"));
+ }
+
+ @Test
+ public void testRotations() {
+ final Set<Rotation> rotations = new HashSet<>();
+ assertThat(new DeployState.Builder().rotations(rotations).build().getRotations().size(), is(0));
+ for (String name : new String[]{"rotation-001.vespa.a02.yahoodns.net", "rotation-002.vespa.a02.yahoodns.net"}) {
+ rotations.add(new Rotation(name));
+ }
+ assertThat(new DeployState.Builder().rotations(rotations).build().getRotations(), equalTo(rotations));
+ }
+
+ private DeployState createDeployState(ApplicationPackage app, final Map<ConfigDefinitionKey, com.yahoo.vespa.config.buildergen.ConfigDefinition> defs) {
+ DeployState.Builder builder = new DeployState.Builder().applicationPackage(app);
+ builder.configDefinitionRepo(new ConfigDefinitionRepo() {
+ @Override
+ public Map<ConfigDefinitionKey, com.yahoo.vespa.config.buildergen.ConfigDefinition> getConfigDefinitions() {
+ return defs;
+ }
+ });
+ return builder.build();
+ }
+}
+
diff --git a/config-model/src/test/java/com/yahoo/config/model/deploy/SystemModelTestCase.java b/config-model/src/test/java/com/yahoo/config/model/deploy/SystemModelTestCase.java
new file mode 100644
index 00000000000..6be85fb19e4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/deploy/SystemModelTestCase.java
@@ -0,0 +1,183 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.deploy;
+
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.test.StandardConfig;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelRegistry;
+import com.yahoo.config.model.MapConfigModelRegistry;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.model.*;
+import com.yahoo.vespa.model.test.ApiConfigModel;
+import com.yahoo.vespa.model.test.SimpleConfigModel;
+import com.yahoo.vespa.model.test.SimpleService;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author bratseth
+ */
+public class SystemModelTestCase {
+
+ private static final String TESTDIR = "src/test/cfg/application/";
+
+ private static VespaModel getVespaModelDoNotValidateXml(String configPath) {
+ ConfigModelRegistry registry = MapConfigModelRegistry.createFromList(new SimpleConfigModel.Builder(), new ApiConfigModel.Builder());
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg(configPath,registry);
+ return creator.create(false); // do not validate against schema -- the xml files used here are not valid
+ }
+
+ // Debugging
+ @SuppressWarnings({"UnusedDeclaration"})
+ private void dumpTree(ConfigProducer producer) {
+ Map<String,? extends ConfigProducer> id2cp = producer.getChildren();
+ for (ConfigProducer c : id2cp.values()) {
+ System.out.println("id: " + c.getConfigId());
+ if (c.getChildren().size() > 0) {
+ dumpTree(c);
+ }
+ }
+ }
+
+ @Test
+ public void testMetrics() {
+ VespaModel vespaModel = getVespaModelDoNotValidateXml(TESTDIR + "metricsconfig");
+ SimpleService service0 = (SimpleService)vespaModel.getConfigProducer("simple/simpleservice.0").get();
+ vespaModel.getConfigProducer("simple/simpleservice.1");
+ assertThat(service0.getDefaultMetricDimensions().get("clustername"), is("testClusterName"));
+ }
+
+ @Test
+ public void testVespaModel() {
+ VespaModel vespaModel = getVespaModelDoNotValidateXml(TESTDIR + "simpleconfig/");
+ assertNotNull(vespaModel);
+
+ assertEquals("There are two instances of the simple model + Routing and AdminModel (set up implicitly)", 4, vespaModel.configModelRepo().asMap().size());
+ assertNotNull("One gets the default name as there is no explicit id", vespaModel.configModelRepo().asMap().get("simple"));
+ assertNotNull("The other gets the explicit id as name", vespaModel.configModelRepo().asMap().get("second"));
+
+ ApplicationConfigProducerRoot root = vespaModel.getVespa();
+ assertNotNull(root);
+
+ // Verify configIds from vespa
+ assertTrue(6 <= root.getConfigIds().size());
+ assertTrue(root.getConfigIds().contains("client"));
+ assertTrue(root.getConfigIds().contains("simple"));
+ assertTrue(root.getConfigIds().contains("second"));
+ assertTrue(root.getConfigIds().contains("simple/simpleservice.0"));
+ assertTrue(root.getConfigIds().contains("simple/simpleservice.1"));
+ assertTrue(root.getConfigIds().contains("second/simpleservice.0"));
+
+ // Verify configIds from vespaModel
+ assertTrue(12 <= vespaModel.getConfigIds().size());
+ String localhost = HostName.getLocalhost();
+ String localhostConfigId = "hosts/" + localhost;
+ Set<String> configIds = vespaModel.getConfigIds();
+ assertTrue(configIds.contains("client"));
+ assertTrue(configIds.contains(localhostConfigId));
+ assertTrue(configIds.contains("simple/simpleservice.0"));
+ assertTrue(configIds.contains("second/simpleservice.0"));
+ assertTrue(configIds.contains("hosts/" + localhost + "/logd"));
+
+ // Verify sentinel config
+ SentinelConfig sentinelConfig = new SentinelConfig((SentinelConfig.Builder) vespaModel.getConfig(new SentinelConfig.Builder(), localhostConfigId));
+ boolean found = false;
+ for (SentinelConfig.Service service : sentinelConfig.service()) {
+ if ("logd".equals(service.name())) {
+ found = true;
+ }
+ }
+ assertTrue(found);
+
+ // Get the simple model config from VespaModel
+ assertEquals(vespaModel.getConfig(StandardConfig.class, "simple/simpleservice.0").astring(), "simpleservice");
+ assertEquals(vespaModel.getConfig(StandardConfig.class, "second/simpleservice.0").astring(), "simpleservice");
+ }
+
+ @Test
+ public void testHostSystem() {
+ VespaModel vespaModel = getVespaModelDoNotValidateXml(TESTDIR + "simpleconfig/");
+ HostSystem hostSystem = vespaModel.getHostSystem();
+
+ HostResource host1 = hostSystem.getHost("host1");
+ HostResource host2 = hostSystem.getHost("host2");
+ HostResource host3 = hostSystem.getHost("host3");
+
+ assertEquals(host1, host2);
+ assertEquals(host2, host3);
+
+ // all three host aliases are for the same host, so the number of services should be 3 + 8
+ // (3 simpleservices and logd, configproxy, config sentinel, admin server config server, slobrok, log server and file distribution)
+ assertEquals(10, host1.getServices().size());
+
+ assertNotNull(host1.getService("simpleservice"));
+ assertNotNull(host1.getService("simpleservice2"));
+ assertNotNull(host3.getService("simpleservice3"));
+ }
+
+ @Test
+ public void testBasePorts() {
+ VespaModel vespaModel = getVespaModelDoNotValidateXml(TESTDIR + "simpleconfig");
+ assertNotNull(vespaModel);
+
+ assertEquals(vespaModel.getConfig(StandardConfig.class, "simple/simpleservice.0").baseport(), 10000);
+ assertTrue(vespaModel.getConfig(StandardConfig.class, "simple/simpleservice.1").baseport() != 10000);
+ }
+
+ /**
+ * This test is the same as the system test cloudconfig/plugins.
+ * Be sure to update it as well if you change this.
+ */
+ @Test
+ public void testPlugins() {
+ VespaModel vespaModel = getVespaModelDoNotValidateXml(TESTDIR + "plugins");
+
+ assertNotNull(vespaModel);
+ ApplicationConfigProducerRoot root = vespaModel.getVespa();
+
+ assertEquals(5, vespaModel.configModelRepo().asMap().size());
+ assertTrue(vespaModel.configModelRepo().asMap().keySet().contains("simple"));
+ assertTrue(vespaModel.configModelRepo().asMap().keySet().contains("api"));
+ assertTrue(root.getConfigIds().contains("simple/simpleservice.0"));
+ assertTrue(root.getConfigIds().contains("simple/simpleservice.1"));
+ assertTrue(root.getConfigIds().contains("api/apiservice.0"));
+
+ // Verify that configModelRegistry iterates in dependency order
+ Iterator<ConfigModel> i = vespaModel.configModelRepo().iterator();
+ ConfigModel plugin = i.next();
+ assertEquals("admin", plugin.getId());
+ plugin = i.next();
+ assertEquals("simple", plugin.getId());
+ plugin = i.next();
+ assertEquals("simple2", plugin.getId());
+ plugin = i.next();
+ assertEquals("api", plugin.getId());
+ plugin = i.next();
+ assertEquals("routing", plugin.getId());
+
+ assertEquals(vespaModel.getConfig(StandardConfig.class, "api/apiservice.0").astring(), "apiservice");
+
+ assertEquals(vespaModel.getConfig(StandardConfig.class, "simple/simpleservice.0").astring(), "simpleservice");
+ assertEquals(vespaModel.getConfig(StandardConfig.class, "simple/simpleservice.1").astring(), "simpleservice");
+ assertEquals(vespaModel.getConfig(StandardConfig.class, "simple2/simpleservice.0").astring(), "simpleservice");
+ }
+
+ @Test
+ public void testEqualPlugins() {
+ try {
+ getVespaModelDoNotValidateXml(TESTDIR + "doubleconfig");
+ fail("No exception upon two plugins with the same name");
+ } catch (RuntimeException expected) {
+ assertThat(expected.getMessage(), is("Could not resolve tag <simpleplugin version=\"1.0\"> to a config model component"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/graph/GraphMock.java b/config-model/src/test/java/com/yahoo/config/model/graph/GraphMock.java
new file mode 100644
index 00000000000..2fd8115a804
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/graph/GraphMock.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.graph;
+
+import com.google.inject.Inject;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class GraphMock {
+
+ public static class BA extends ConfigModelBuilder<A> {
+ public BA() { super(A.class); }
+ @Override public List<ConfigModelId> handlesElements() { return Arrays.asList(); }
+ @Override public void doBuild(A model, Element spec, ConfigModelContext modelContext) { }
+ }
+ public static class A extends ConfigModel {
+ public A(ConfigModelContext modelContext) { super(modelContext); }
+ }
+
+ public static class BB extends ConfigModelBuilder<B> {
+ public BB() { super(B.class); }
+ @Override public List<ConfigModelId> handlesElements() { return Arrays.asList(); }
+ @Override public void doBuild(B model, Element spec, ConfigModelContext modelContext) { }
+ }
+ public static class B extends ConfigModel {
+ public final A a;
+ @Inject
+ public B(ConfigModelContext modelContext, A modelA) { super(modelContext); this.a = modelA; }
+ }
+
+ public static class BC extends ConfigModelBuilder<C> {
+ public BC() { super(C.class); }
+ @Override public List<ConfigModelId> handlesElements() { return Arrays.asList(); }
+ @Override public void doBuild(C model, Element spec, ConfigModelContext modelContext) { }
+ }
+ public static class C extends ConfigModel {
+ public Collection<B> b;
+ public A a;
+ public C(ConfigModelContext modelContext, Collection<B> modelB, A modelA) { super(modelContext); b = modelB; a = modelA; }
+ }
+
+ public static class BD extends ConfigModelBuilder<D> {
+ public BD() { super(D.class); }
+ @Override public List<ConfigModelId> handlesElements() { return Arrays.asList(); }
+ @Override public void doBuild(D model, Element spec, ConfigModelContext modelContext) { }
+ }
+ public static class D extends ConfigModel {
+ public D(ConfigModelContext modelContext, E modelE) { super(modelContext); }
+ }
+
+ public static class BE extends ConfigModelBuilder<E> {
+ public BE() { super(E.class); }
+ @Override public List<ConfigModelId> handlesElements() { return Arrays.asList(); }
+ @Override public void doBuild(E model, Element spec, ConfigModelContext modelContext) { }
+ }
+ public static class E extends ConfigModel {
+ public E(ConfigModelContext modelContext, D modelD) { super(modelContext); }
+ }
+
+ public static class Bad extends ConfigModel {
+ public Bad() { super(null); }
+ public static class Builder extends ConfigModelBuilder<Bad> {
+ public Builder() { super(Bad.class); }
+ @Override public List<ConfigModelId> handlesElements() { return null; }
+ @Override public void doBuild(Bad model, Element spec, ConfigModelContext modelContext) { }
+ }
+ }
+
+ public static class Bad2 extends ConfigModel {
+ public Bad2(ConfigModelContext ctx, String foo) { super(ctx); }
+ public static class Builder extends ConfigModelBuilder<Bad2> {
+ public Builder() { super(Bad2.class); }
+ @Override public List<ConfigModelId> handlesElements() { return null; }
+ @Override public void doBuild(Bad2 model, Element spec, ConfigModelContext modelContext) { }
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/graph/ModelGraphTest.java b/config-model/src/test/java/com/yahoo/config/model/graph/ModelGraphTest.java
new file mode 100644
index 00000000000..6970c27eca2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/graph/ModelGraphTest.java
@@ -0,0 +1,118 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.graph;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.test.MockRoot;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.List;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class ModelGraphTest {
+
+ void assertOrdering(ModelGraph graph, String expectedOrdering) {
+ List<ModelNode> sortedEntries = graph.topologicalSort();
+ StringBuilder sb = new StringBuilder();
+ for (ModelNode<?> node : sortedEntries) {
+ sb.append(node.builder.getModelClass().getSimpleName());
+ }
+ assertThat(sb.toString(), is(expectedOrdering));
+ }
+
+ @Test
+ public void require_that_dependencies_are_correctly_set() {
+ ModelGraphBuilder builder = new ModelGraphBuilder();
+ builder.addBuilder(new GraphMock.BC()).addBuilder(new GraphMock.BB()).addBuilder(new GraphMock.BA());
+ ModelGraph graph = builder.build();
+ List<ModelNode> nodes = graph.getNodes();
+ assertThat(graph.getNodes().size(), is(3));
+ assertTrue(nodes.get(0).hasDependencies());
+ assertTrue(nodes.get(1).hasDependencies());
+ assertFalse(nodes.get(2).hasDependencies());
+ assertTrue(nodes.get(0).dependsOn(nodes.get(1)));
+ assertTrue(nodes.get(1).dependsOn(nodes.get(2)));
+ assertFalse(nodes.get(2).dependsOn(nodes.get(0)));
+ }
+
+ @Test
+ public void require_that_dependencies_are_correctly_sorted() {
+ ModelGraph graph = new ModelGraphBuilder().addBuilder(new GraphMock.BC()).addBuilder(new GraphMock.BB()).addBuilder(new GraphMock.BA()).build();
+ assertOrdering(graph, "ABC");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_that_cycles_are_detected() {
+ ModelGraph graph = new ModelGraphBuilder().addBuilder(new GraphMock.BD()).addBuilder(new GraphMock.BE()).build();
+ assertThat(graph.getNodes().size(), is(2));
+ assertTrue(graph.getNodes().get(0).dependsOn(graph.getNodes().get(1)));
+ assertTrue(graph.getNodes().get(1).dependsOn(graph.getNodes().get(0)));
+ graph.topologicalSort();
+ }
+
+ @Test
+ public void require_that_instance_can_be_created() {
+ ModelGraph graph = new ModelGraphBuilder().addBuilder(new GraphMock.BC()).addBuilder(new GraphMock.BB()).addBuilder(new GraphMock.BA()).build();
+ List<ModelNode> nodes = graph.topologicalSort();
+ MockRoot root = new MockRoot();
+ GraphMock.A a = (GraphMock.A) nodes.get(0).createModel(ConfigModelContext.createFromParentAndId(null, root, "first"));
+ GraphMock.B b = (GraphMock.B) nodes.get(1).createModel(ConfigModelContext.createFromParentAndId(null, root, "second"));
+ GraphMock.B b2 = (GraphMock.B) nodes.get(1).createModel(ConfigModelContext.createFromParentAndId(null, root, "second2"));
+ GraphMock.C c = (GraphMock.C) nodes.get(2).createModel(ConfigModelContext.createFromParentAndId(null, root, "third"));
+ assertNotNull(a);
+ assertNotNull(b);
+ assertNotNull(b2);
+ assertNotNull(c);
+ assertThat(a.getId(), is("first"));
+ assertThat(b.getId(), is("second"));
+ assertThat(b2.getId(), is("second2"));
+ assertThat(c.getId(), is("third"));
+ assertThat(b.a, is(a));
+ assertNotNull(c.b);
+ assertThat(c.b.size(), is(2));
+ assertTrue(c.b.contains(b));
+ assertTrue(c.b.contains(b2));
+ for (ConfigModel m : c.b) {
+ System.out.println(m.getId());
+ }
+ }
+
+ @Rule
+ public ExpectedException expectedEx = ExpectedException.none();
+
+ @Test
+ public void require_that_context_must_be_first_ctor_param() {
+ expectedEx.expect(IllegalArgumentException.class);
+ expectedEx.expectMessage("Constructor for " + GraphMock.Bad.class.getName() + " must have as its first argument a " + ConfigModelContext.class.getName());
+ ModelNode node = new ModelNode(new GraphMock.Bad.Builder());
+ node.createModel(ConfigModelContext.createFromParentAndId(null, new MockRoot(), "foo"));
+ }
+
+ @Test
+ public void require_that_ctor_arguments_must_be_models_or_collections_of_models() {
+ expectedEx.expect(IllegalArgumentException.class);
+ expectedEx.expectMessage("Unable to find constructor argument class java.lang.String for com.yahoo.config.model.graph.GraphMock$Bad2");
+ ModelNode node = new ModelNode(new GraphMock.Bad2.Builder());
+ node.createModel(ConfigModelContext.createFromParentAndId(null, new MockRoot(), "foo"));
+ }
+
+ @Test
+ public void require_that_collections_can_be_empty() {
+ ModelGraph graph = new ModelGraphBuilder().addBuilder(new GraphMock.BC()).addBuilder(new GraphMock.BA()).build();
+ List<ModelNode> nodes = graph.topologicalSort();
+ MockRoot root = new MockRoot();
+ GraphMock.A a = (GraphMock.A) nodes.get(0).createModel(ConfigModelContext.createFromParentAndId(null, root, "first"));
+ GraphMock.C c = (GraphMock.C) nodes.get(1).createModel(ConfigModelContext.createFromParentAndId(null, root, "second"));
+ assertThat(c.a, is(a));
+ assertTrue(c.b.isEmpty());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/producer/AbstractConfigProducerTest.java b/config-model/src/test/java/com/yahoo/config/model/producer/AbstractConfigProducerTest.java
new file mode 100644
index 00000000000..86b0a9d05d6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/producer/AbstractConfigProducerTest.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.producer;
+
+import com.yahoo.cloud.config.log.LogdConfig;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Verifies some of the logic in the abstract config producer that is not tested in other classes.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class AbstractConfigProducerTest {
+
+ @Test
+ public void require_that_interface_is_found_if_directly_implemented() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+ MockLogdProducer producer = new MockLogdProducer("mocky");
+ ClassLoader loader = producer.getConfigClassLoader(LogdConfig.Producer.class.getName());
+ assertNotNull(loader);
+ Class clazz = loader.loadClass(LogdConfig.Builder.class.getName());
+ LogdConfig.Builder builder = (LogdConfig.Builder) clazz.newInstance();
+ producer.getConfig(builder);
+ LogdConfig config = new LogdConfig(builder);
+ assertThat(config.logserver().host(), is("bar"));
+ assertThat(config.logserver().port(), is(1338));
+ }
+
+ @Test
+ public void require_that_interface_is_found_if_inherited() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+ MockLogdProducerSubclass producer = new MockLogdProducerSubclass("mocky");
+ ClassLoader loader = producer.getConfigClassLoader(LogdConfig.Producer.class.getName());
+ assertNotNull(loader);
+ Class clazz = loader.loadClass(LogdConfig.Builder.class.getName());
+ LogdConfig.Builder builder = (LogdConfig.Builder) clazz.newInstance();
+ producer.getConfig(builder);
+ LogdConfig config = new LogdConfig(builder);
+ assertThat(config.logserver().host(), is("foo"));
+ assertThat(config.logserver().port(), is(1337));
+ }
+
+ private static class MockLogdProducer extends AbstractConfigProducer implements LogdConfig.Producer {
+
+ public MockLogdProducer(String subId) {
+ super(subId);
+ }
+
+ @Override
+ public void getConfig(LogdConfig.Builder builder) {
+ builder.logserver(new LogdConfig.Logserver.Builder().host("bar").port(1338));
+ }
+ }
+
+ private static abstract class MockLogdSuperClass extends AbstractConfigProducer implements LogdConfig.Producer {
+
+ public MockLogdSuperClass(String subId) {
+ super(subId);
+ }
+ }
+
+ private static class MockLogdProducerSubclass extends MockLogdSuperClass {
+ public MockLogdProducerSubclass(String subId) {
+ super(subId);
+ }
+
+ @Override
+ public void getConfig(LogdConfig.Builder builder) {
+ builder.logserver(new LogdConfig.Logserver.Builder().host("foo").port(1337));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/provision/HostSpecTest.java b/config-model/src/test/java/com/yahoo/config/model/provision/HostSpecTest.java
new file mode 100644
index 00000000000..7a8e30d4a66
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/provision/HostSpecTest.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import com.yahoo.config.provision.HostSpec;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.11
+ */
+public class HostSpecTest {
+ @Test
+ public void testEquals() {
+ HostSpec h1 = new HostSpec("foo", Collections.<String>emptyList());
+ HostSpec h2 = new HostSpec("foo", Collections.<String>emptyList());
+ HostSpec h3 = new HostSpec("foo", Arrays.asList("my", "alias"));
+ HostSpec h4 = new HostSpec("bar", Collections.<String>emptyList());
+
+ assertTrue(h1.equals(h1));
+ assertTrue(h1.equals(h2));
+ assertTrue(h1.equals(h3));
+ assertFalse(h1.equals(h4));
+
+ assertTrue(h2.equals(h1));
+ assertTrue(h2.equals(h2));
+ assertTrue(h2.equals(h3));
+ assertFalse(h2.equals(h4));
+
+ assertTrue(h3.equals(h1));
+ assertTrue(h3.equals(h2));
+ assertTrue(h3.equals(h3));
+ assertFalse(h3.equals(h4));
+
+ assertFalse(h4.equals(h1));
+ assertFalse(h4.equals(h2));
+ assertFalse(h4.equals(h3));
+ assertTrue(h4.equals(h4));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/provision/HostsXmlProvisionerTest.java b/config-model/src/test/java/com/yahoo/config/model/provision/HostsXmlProvisionerTest.java
new file mode 100644
index 00000000000..2a0a0bf224b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/provision/HostsXmlProvisionerTest.java
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import com.yahoo.config.provision.HostSpec;
+import com.yahoo.vespa.model.container.Container;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.util.*;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author musum
+ */
+public class HostsXmlProvisionerTest {
+ private static final String oneHost = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<hosts>\n" +
+ " <host name=\"test1.yahoo.com\">\n" +
+ " <alias>node1</alias>\n" +
+ " <alias>node2</alias>\n" +
+ " </host>\n" +
+ "</hosts>";
+
+ private static final String threeHosts = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
+ "<hosts>\n" +
+ " <host name=\"test1.yahoo.com\">\n" +
+ " <alias>node1</alias>\n" +
+ " </host>\n" +
+ " <host name=\"test2.yahoo.com\">\n" +
+ " <alias>node2</alias>\n" +
+ " <alias>node3</alias>\n" +
+ " </host>\n" +
+ " <host name=\"test3.yahoo.com\">\n" +
+ " <alias>node4</alias>\n" +
+ " </host>\n" +
+ "</hosts>";
+
+ @Test
+ public void require_basic_works() {
+ HostsXmlProvisioner hostProvisioner = createProvisioner(threeHosts);
+
+ // 4 services, 2 host aliases, mapping to 2 host.
+ List<String> aliases = createAliases();
+ Map<String, HostSpec> map = allocate(hostProvisioner, aliases);
+
+ assertCorrectNumberOfHosts(map, 2);
+ for (HostSpec hostSpec : map.values()) {
+ if (hostSpec.hostname().equals("test2.yahoo.com")) {
+ assertThat(hostSpec.aliases().size(), is(2));
+ } else {
+ assertThat(hostSpec.aliases().size(), is(1));
+ }
+ }
+ assertThat(map.size(), is(2));
+ assertTrue(map.keySet().containsAll(aliases));
+
+ // 5 services, 3 host aliases, mapping to 2 host.
+ aliases = createAliases(Collections.singletonList("node3"));
+ map = allocate(hostProvisioner, aliases);
+
+ assertCorrectNumberOfHosts(map, 2);
+ assertThat(map.size(), is(3));
+ assertTrue(map.keySet().containsAll(aliases));
+
+ // 5 services, 3 host aliases, mapping to 3 host.
+ aliases = createAliases(Collections.singletonList("node4"));
+ map = allocate(hostProvisioner, aliases);
+ assertThat(map.size(), is(3));
+ assertCorrectNumberOfHosts(map, 3);
+ assertTrue(map.keySet().containsAll(aliases));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_exception_when_unknown_hosts_alias() {
+ HostsXmlProvisioner hostProvisioner = createProvisioner(oneHost);
+ hostProvisioner.allocateHost("unknown");
+ }
+
+ private void assertCorrectNumberOfHosts(Map<String, HostSpec> hostToServiceMap, int expectedHostCount) {
+ Set<String> hostSet = new HashSet<>();
+ for (HostSpec host : hostToServiceMap.values()) {
+ hostSet.add(host.hostname());
+ }
+ assertThat(hostSet.size(), is(expectedHostCount));
+ }
+
+ private HostsXmlProvisioner createProvisioner(String hosts) {
+ return new HostsXmlProvisioner(new StringReader(hosts));
+ }
+
+ private List<String> createAliases() {
+ return createAliases(new ArrayList<>());
+ }
+
+ // Admin services on node1, qrserver on node2 + additional specs
+ private List<String> createAliases(Collection<String> additionalAliases) {
+ ArrayList<String> aliases = new ArrayList<>();
+ aliases.add("node1");
+ aliases.add("node1");
+ aliases.add("node1");
+ aliases.add("node2");
+ aliases.addAll(additionalAliases);
+ return aliases;
+ }
+
+ private Map<String, HostSpec> allocate(HostsXmlProvisioner hostProvisioner, List<String> aliases) {
+ Map<String, HostSpec> map = new LinkedHashMap<>();
+ for (String alias : aliases) {
+ map.put(alias, hostProvisioner.allocateHost(alias));
+ }
+ return map;
+ }
+
+ @Test
+ public void require_singlenode_HostAlias_is_used_if_hosts_xml() {
+ String servicesXml = "<jdisc id='default' version='1.0' />";
+ HostsXmlProvisioner hostProvisioner = createProvisioner(oneHost);
+ HostSpec hostSpec = hostProvisioner.allocateHost(Container.SINGLENODE_CONTAINER_SERVICESPEC);
+ assertThat(hostSpec.hostname(), is("test1.yahoo.com"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java b/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java
new file mode 100644
index 00000000000..b74398fc4ae
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/provision/ModelProvisioningTest.java
@@ -0,0 +1,1108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+import java.io.StringReader;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.HostSystem;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.admin.Slobrok;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.search.Dispatch;
+import org.junit.Test;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.content.StorageNode;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+
+/**
+ * Test cases for provisioning nodes to entire vespamodels
+ *
+ * @author vegardh
+ * @author bratseth
+ */
+public class ModelProvisioningTest {
+
+ private Hosts createHosts(int n) { return createHosts("", n); }
+ private Hosts createHosts(String hostnamePrefix, int n) {
+ Hosts hosts = new Hosts();
+ if ( ! hostnamePrefix.isEmpty())
+ hostnamePrefix = "-" + hostnamePrefix;
+ for (int i = 0; i < n; i++)
+ hosts.addHost(new com.yahoo.config.model.provision.Host(hostnamePrefix + "foo" + i), Collections.emptyList());
+ return hosts;
+ }
+
+ /** Creates a model with hosts of the 'default' flavor */
+ private VespaModel createModel(String services, Hosts hosts, boolean failOnOutOfCapacity, String ... retiredHostNames) throws ParseException {
+ return createModel(services, Collections.singletonMap("default", hosts.getHosts()), failOnOutOfCapacity, 0, retiredHostNames);
+ }
+
+ /**
+ * Creates a model
+ *
+ * @param services the services xml string
+ * @param hosts hosts by flavor
+ * @param failOnOutOfCapacity whether we should get an exception when not enough hosts of the requested flavor
+ * is available or if we should just silently receive a smaller allocation
+ * @return the resulting model
+ * @throws ParseException if the services xml is invalid
+ */
+ private VespaModel createModel(String services, Map<String, Collection<Host>> hosts, boolean failOnOutOfCapacity, int startIndexForClusters, String ... retiredHostNames) throws ParseException {
+ final VespaModelCreatorWithMockPkg modelCreatorWithMockPkg = new VespaModelCreatorWithMockPkg(null, services, ApplicationPackageUtils.generateSearchDefinition("type1"));
+ final ApplicationPackage appPkg = modelCreatorWithMockPkg.appPkg;
+ DeployState deployState = new DeployState.Builder().applicationPackage(appPkg).modelHostProvisioner(new InMemoryProvisioner(hosts, failOnOutOfCapacity, startIndexForClusters, retiredHostNames)).
+ properties((new DeployProperties.Builder()).hostedVespa(true).build()).build();
+ return modelCreatorWithMockPkg.create(false, deployState);
+ }
+
+ private void assertCorrectModel(VespaModel model, int numberOfHosts, int numberOfContentNodes) {
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+ final Map<String, ContentCluster> contentClusters = model.getContentClusters();
+ ContentCluster cluster = contentClusters.get("bar");
+ assertThat(cluster.getRootGroup().getNodes().size(), is(numberOfContentNodes));
+ int i = 0;
+ for (StorageNode node : cluster.getRootGroup().getNodes()) {
+ assertThat(node.getDistributionKey(), is(i));
+ i++;
+ }
+ }
+
+ @Test
+ public void testNodeCountForJdisc() {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>\n" +
+ "\n" +
+ "<admin version='3.0'><nodes count='1' /></admin>\n" +
+ "<jdisc id='mydisc' version='1.0'>" +
+ " <handler id='myHandler'>" +
+ " <component id='injected' />" +
+ " </handler>" +
+ " <nodes count=\"3\"/>" +
+ "</jdisc>" +
+ "<jdisc id='mydisc2' version='1.0'>" +
+ " <handler id='myHandler'>" +
+ " <component id='injected' />" +
+ " </handler>" +
+ " <nodes count='2' jvmargs='-verbosegc' preload='lib/blablamalloc.so'/>" +
+ "</jdisc>" +
+ "</services>";
+ String hosts ="<hosts>"
+ + " <host name='myhost0'>"
+ + " <alias>node0</alias>"
+ + " </host>"
+ + " <host name='myhost1'>"
+ + " <alias>node1</alias>"
+ + " </host>"
+ + " <host name='myhost2'>"
+ + " <alias>node2</alias>"
+ + " </host>"
+ + " <host name='myhost3'>"
+ + " <alias>node3</alias>"
+ + " </host>"
+ + " <host name='myhost4'>"
+ + " <alias>node4</alias>"
+ + " </host>"
+ + " <host name='myhost5'>"
+ + " <alias>node5</alias>"
+ + " </host>"
+ + "</hosts>";
+ VespaModelCreatorWithMockPkg creator = new VespaModelCreatorWithMockPkg(null, services);
+ VespaModel model = creator.create(new DeployState.Builder().modelHostProvisioner(new InMemoryProvisioner(Hosts.getHosts(new StringReader(hosts)), true)));
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().size(), is(3));
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(0).getConfigId(), is("mydisc/container.0"));
+ assertTrue(model.getContainerClusters().get("mydisc").getContainers().get(0).isInitialized());
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(1).getConfigId(), is("mydisc/container.1"));
+ assertTrue(model.getContainerClusters().get("mydisc").getContainers().get(1).isInitialized());
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(2).getConfigId(), is("mydisc/container.2"));
+ assertTrue(model.getContainerClusters().get("mydisc").getContainers().get(2).isInitialized());
+
+ assertThat(model.getContainerClusters().get("mydisc2").getContainers().size(), is(2));
+ assertThat(model.getContainerClusters().get("mydisc2").getContainers().get(0).getConfigId(), is("mydisc2/container.0"));
+ assertTrue(model.getContainerClusters().get("mydisc2").getContainers().get(0).isInitialized());
+ assertThat(model.getContainerClusters().get("mydisc2").getContainers().get(1).getConfigId(), is("mydisc2/container.1"));
+ assertTrue(model.getContainerClusters().get("mydisc2").getContainers().get(1).isInitialized());
+
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(0).getJvmArgs(), is(""));
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(1).getJvmArgs(), is(""));
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(2).getJvmArgs(), is(""));
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(0).getPreLoad(), is(Defaults.getDefaults().vespaHome() + "lib64/vespa/malloc/libvespamalloc.so"));
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(1).getPreLoad(), is(Defaults.getDefaults().vespaHome() + "lib64/vespa/malloc/libvespamalloc.so"));
+ assertThat(model.getContainerClusters().get("mydisc").getContainers().get(2).getPreLoad(), is(Defaults.getDefaults().vespaHome() + "lib64/vespa/malloc/libvespamalloc.so"));
+
+ assertThat(model.getContainerClusters().get("mydisc2").getContainers().get(0).getJvmArgs(), is("-verbosegc"));
+ assertThat(model.getContainerClusters().get("mydisc2").getContainers().get(1).getJvmArgs(), is("-verbosegc"));
+ assertThat(model.getContainerClusters().get("mydisc2").getContainers().get(0).getPreLoad(), is("lib/blablamalloc.so"));
+ assertThat(model.getContainerClusters().get("mydisc2").getContainers().get(1).getPreLoad(), is("lib/blablamalloc.so"));
+
+ final HostSystem hostSystem = model.getHostSystem();
+ assertNotNull(hostSystem.getHostByHostname("myhost0"));
+ assertNotNull(hostSystem.getHostByHostname("myhost1"));
+ assertNotNull(hostSystem.getHostByHostname("myhost2"));
+ assertNotNull(hostSystem.getHostByHostname("myhost3"));
+ assertNull(hostSystem.getHostByHostname("Nope"));
+ }
+
+ @Test
+ public void testNodeCountForContentGroup() throws Exception {
+ String xmlWithNodes =
+ "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services>" +
+ "\n" +
+ " <admin version='3.0'>" +
+ " <nodes count='3'/>" +
+ " </admin>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='2'/>" +
+ " </content>" +
+ "</services>";
+ int numberOfHosts = 2;
+ Hosts hosts = createHosts(numberOfHosts);
+ int numberOfContentNodes = 2;
+ VespaModel model = createModel(xmlWithNodes, hosts, true);
+ assertCorrectModel(model, numberOfHosts, numberOfContentNodes);
+ }
+
+ @Test
+ public void testNodeCountForContentGroupHierarchy() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version='3.0'>\n" +
+ " <nodes count='3'/>" + // Ignored
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>\n" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <group>" +
+ " <distribution partitions=\"1|*\"/>" +
+ " <group name='0' distribution-key='0'>" +
+ " <nodes count='2'/> " +
+ " </group>" +
+ " <group name='1' distribution-key='1'>" +
+ " <nodes count='2'/> " +
+ " </group>" +
+ " </group>" +
+ " </content>" +
+ " <content version='1.0' id='baz'>" +
+ " <redundancy>2</redundancy>\n" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <group>" +
+ " <distribution partitions=\"1|*\"/>" +
+ " <group name='0' distribution-key='10'>" +
+ " <nodes count='1'/> " +
+ " </group>" +
+ " <group name='1' distribution-key='11'>" +
+ " <nodes count='1'/> " +
+ " </group>" +
+ " </group>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+ int numberOfHosts = 6;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ assertThat(cluster.getRootGroup().getNodes().size(), is(0));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("bar/storage/0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getDistributionKey(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getConfigId(), is("bar/storage/1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("bar/storage/2"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getDistributionKey(), is(3));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getConfigId(), is("bar/storage/3"));
+
+ cluster = model.getContentClusters().get("baz");
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("10"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("baz/storage/0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("11"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("baz/storage/1"));
+ }
+
+ @Test
+ public void testUsingNodesAndGroupCountAttributes() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='27' groups='9'/>" +
+ " </content>" +
+ " <content version='1.0' id='baz'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='27' groups='27'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 64;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check container cluster
+ assertEquals(1, model.getContainerClusters().size());
+ Set<com.yahoo.vespa.model.Host> containerHosts = model.getContainerClusters().get("foo").getContainers().stream().map(Container::getHost).collect(Collectors.toSet());
+ assertEquals(10, containerHosts.size());
+
+ // Check admin clusters
+ Admin admin = model.getAdmin();
+ Set<com.yahoo.vespa.model.Host> slobrokHosts = admin.getSlobroks().stream().map(Slobrok::getHost).collect(Collectors.toSet());
+ assertEquals(3, slobrokHosts.size());
+ assertTrue("Slobroks are assigned from container nodes", containerHosts.containsAll(slobrokHosts));
+ assertTrue("Logserver is assigned from container nodes", containerHosts.contains(admin.getLogserver().getHost()));
+ assertEquals("No in-cluster config servers in a hosted environment", 0, admin.getConfigservers().size());
+ assertEquals("No admin cluster controller when multitenant", null, admin.getClusterControllers());
+
+ // Check content clusters
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ ContainerCluster clusterControllers = cluster.getClusterControllers();
+ assertEquals(3, clusterControllers.getContainers().size());
+ assertEquals("bar-controllers", clusterControllers.getName());
+ assertEquals("foo10", clusterControllers.getContainers().get(0).getHostName());
+ assertEquals("foo13", clusterControllers.getContainers().get(1).getHostName());
+ assertEquals("foo16", clusterControllers.getContainers().get(2).getHostName());
+ assertEquals(0, cluster.getRootGroup().getNodes().size());
+ assertEquals(9, cluster.getRootGroup().getSubgroups().size());
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(3));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("bar/storage/0"));
+ assertEquals("foo10", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName());
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getDistributionKey(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getConfigId(), is("bar/storage/1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(2).getDistributionKey(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(2).getConfigId(), is("bar/storage/2"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(3));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(3));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("bar/storage/3"));
+ assertEquals("foo13", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName());
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getDistributionKey(), is(4));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getConfigId(), is("bar/storage/4"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(2).getDistributionKey(), is(5));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(2).getConfigId(), is("bar/storage/5"));
+ // ...
+ assertEquals("foo16", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName());
+ // ...
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getIndex(), is("8"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().size(), is(3));
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().get(0).getDistributionKey(), is(24));
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().get(0).getConfigId(), is("bar/storage/24"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().get(1).getDistributionKey(), is(25));
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().get(1).getConfigId(), is("bar/storage/25"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().get(2).getDistributionKey(), is(26));
+ assertThat(cluster.getRootGroup().getSubgroups().get(8).getNodes().get(2).getConfigId(), is("bar/storage/26"));
+
+ cluster = model.getContentClusters().get("baz");
+ clusterControllers = cluster.getClusterControllers();
+ assertEquals(3, clusterControllers.getContainers().size());
+ assertEquals("baz-controllers", clusterControllers.getName());
+ assertEquals("foo37", clusterControllers.getContainers().get(0).getHostName());
+ assertEquals("foo38", clusterControllers.getContainers().get(1).getHostName());
+ assertEquals("foo39", clusterControllers.getContainers().get(2).getHostName());
+ assertEquals(0, cluster.getRootGroup().getNodes().size());
+ assertEquals(27, cluster.getRootGroup().getSubgroups().size());
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("baz/storage/0"));
+ assertEquals("foo37", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName());
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("baz/storage/1"));
+ assertEquals("foo38", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName());
+ // ...
+ assertEquals("foo39", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName());
+ // ...
+ assertThat(cluster.getRootGroup().getSubgroups().get(26).getIndex(), is("26"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(26).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(26).getNodes().get(0).getDistributionKey(), is(26));
+ assertThat(cluster.getRootGroup().getSubgroups().get(26).getNodes().get(0).getConfigId(), is("baz/storage/26"));
+ }
+
+ @Test
+ public void testGroupsOfSize1() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='8' groups='8'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 18;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check content cluster
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ ContainerCluster clusterControllers = cluster.getClusterControllers();
+ assertEquals(3, clusterControllers.getContainers().size());
+ assertEquals("bar-controllers", clusterControllers.getName());
+ assertEquals("foo10", clusterControllers.getContainers().get(0).getHostName());
+ assertEquals("foo11", clusterControllers.getContainers().get(1).getHostName());
+ assertEquals("foo12", clusterControllers.getContainers().get(2).getHostName());
+ assertEquals(0, cluster.getRootGroup().getNodes().size());
+ assertEquals(8, cluster.getRootGroup().getSubgroups().size());
+ assertEquals(8, cluster.distributionBits());
+ // first group
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("bar/storage/0"));
+ assertEquals("foo10", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName());
+ // second group
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("bar/storage/1"));
+ assertEquals("foo11", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName());
+ // ... last group
+ assertThat(cluster.getRootGroup().getSubgroups().get(7).getIndex(), is("7"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(7).getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(7).getNodes().get(0).getDistributionKey(), is(7));
+ assertThat(cluster.getRootGroup().getSubgroups().get(7).getNodes().get(0).getConfigId(), is("bar/storage/7"));
+ assertEquals("foo17", cluster.getRootGroup().getSubgroups().get(7).getNodes().get(0).getHostName());
+ }
+
+ @Test
+ public void testExplicitNonDedicatedClusterControllers() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <controllers><nodes dedicated='false' count='6'/></controllers>" +
+ " <nodes count='9' groups='3'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 19;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check content clusters
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ ContainerCluster clusterControllers = cluster.getClusterControllers();
+ assertEquals( 8, cluster.distributionBits());
+ assertEquals("We get the closest odd numer", 5, clusterControllers.getContainers().size());
+ assertEquals("bar-controllers", clusterControllers.getName());
+ assertEquals("foo10", clusterControllers.getContainers().get(0).getHostName());
+ assertEquals("foo11", clusterControllers.getContainers().get(1).getHostName());
+ assertEquals("foo13", clusterControllers.getContainers().get(2).getHostName());
+ assertEquals("foo14", clusterControllers.getContainers().get(3).getHostName()); // Should be 16 for perfect distribution ...
+ assertEquals("foo10", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getHostName());
+ assertEquals("foo11", cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getHostName());
+ assertEquals("foo13", cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getHostName());
+ assertEquals("foo16", cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getHostName());
+ }
+
+ @Test
+ public void testClusterControllersAreNotPlacedOnRetiredNodes() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='9' groups='3'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 19;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true, "foo10", "foo13", "foo16");
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check content clusters
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ ContainerCluster clusterControllers = cluster.getClusterControllers();
+ assertEquals(3, clusterControllers.getContainers().size());
+ assertEquals("bar-controllers", clusterControllers.getName());
+ assertEquals("Skipping retired foo10", "foo11", clusterControllers.getContainers().get(0).getHostName());
+ assertEquals("Skipping retired foo13", "foo14", clusterControllers.getContainers().get(1).getHostName());
+ assertEquals("Skipping retired foo16", "foo17", clusterControllers.getContainers().get(2).getHostName());
+ }
+
+ @Test
+ public void testSlobroksClustersAreExpandedToIncludeRetiredNodes() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ "</services>";
+
+ int numberOfHosts = 10;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true, "foo0");
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check slobroks clusters
+ assertEquals("Includes retired node", 1+3, model.getAdmin().getSlobroks().size());
+ assertEquals("foo1", model.getAdmin().getSlobroks().get(0).getHostName());
+ assertEquals("foo2", model.getAdmin().getSlobroks().get(1).getHostName());
+ assertEquals("foo3", model.getAdmin().getSlobroks().get(2).getHostName());
+ assertEquals("Included in addition because it is retired", "foo0", model.getAdmin().getSlobroks().get(3).getHostName());
+ }
+
+ @Test
+ public void testSlobroksClustersAreExpandedToIncludeRetiredNodesWhenRetiredComesLast() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ "</services>";
+
+ int numberOfHosts = 10;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true, "foo3", "foo4");
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check slobroks clusters
+ assertEquals("Includes retired node", 3+2, model.getAdmin().getSlobroks().size());
+ assertEquals("foo0", model.getAdmin().getSlobroks().get(0).getHostName());
+ assertEquals("foo1", model.getAdmin().getSlobroks().get(1).getHostName());
+ assertEquals("foo2", model.getAdmin().getSlobroks().get(2).getHostName());
+ assertEquals("Included in addition because it is retired", "foo3", model.getAdmin().getSlobroks().get(3).getHostName());
+ assertEquals("Included in addition because it is retired", "foo4", model.getAdmin().getSlobroks().get(4).getHostName());
+ }
+
+ @Test
+ public void testSlobroksAreSpreadOverAllContainerClusters() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <container version='1.0' id='bar'>" +
+ " <nodes count='3'/>" +
+ " </container>" +
+ "</services>";
+
+ int numberOfHosts = 13;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true, "foo0", "foo10", "foo11");
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check slobroks clusters
+ // ... from cluster foo
+ assertEquals("Includes retired node", 3+3, model.getAdmin().getSlobroks().size());
+ assertEquals("foo1", model.getAdmin().getSlobroks().get(0).getHostName());
+ assertEquals("foo2", model.getAdmin().getSlobroks().get(1).getHostName());
+ assertEquals("Included in addition because it is retired", "foo0", model.getAdmin().getSlobroks().get(2).getHostName());
+ // ... from cluster bar
+ assertEquals("foo12", model.getAdmin().getSlobroks().get(3).getHostName());
+ assertEquals("Included in addition because it is retired", "foo10", model.getAdmin().getSlobroks().get(4).getHostName());
+ assertEquals("Included in addition because it is retired", "foo11", model.getAdmin().getSlobroks().get(5).getHostName());
+ }
+
+ @Test
+ public void test2ContentNodesProduces1ClusterController() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='2'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 2;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ ContainerCluster clusterControllers = cluster.getClusterControllers();
+ assertEquals(1, clusterControllers.getContainers().size());
+ }
+
+ @Test
+ public void testExplicitDedicatedClusterControllers() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <container version='1.0' id='foo'>" +
+ " <nodes count='10'/>" +
+ " </container>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <controllers><nodes dedicated='true' count='4'/></controllers>" +
+ " <nodes count='9' groups='3'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 23;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ // Check content clusters
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ ContainerCluster clusterControllers = cluster.getClusterControllers();
+ assertEquals(4, clusterControllers.getContainers().size());
+ assertEquals("bar-controllers", clusterControllers.getName());
+ assertEquals("foo19", clusterControllers.getContainers().get(0).getHostName());
+ assertEquals("foo20", clusterControllers.getContainers().get(1).getHostName());
+ assertEquals("foo21", clusterControllers.getContainers().get(2).getHostName());
+ assertEquals("foo22", clusterControllers.getContainers().get(3).getHostName());
+ }
+
+ @Test
+ public void testUsingNodesAndGroupCountAttributesAndGettingTooFewNodes() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services>" +
+ " <admin version='3.0'>" +
+ " <nodes count='3'/>" + // Ignored
+ " </admin>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy reply-after='3'>4</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='24' groups='3'/>" +
+ " <engine><proton><searchable-copies>3</searchable-copies></proton></engine>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 6; // We only have 6 content nodes -> 3 groups with redundancy 2 in each
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, false);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ assertEquals(2*3, cluster.redundancy().effectiveInitialRedundancy()); // Reduced from 3*3
+ assertEquals(2*3, cluster.redundancy().effectiveFinalRedundancy()); // Reduced from 3*4
+ assertEquals(2*3, cluster.redundancy().effectiveReadyCopies()); // Reduced from 3*3
+ assertEquals("2|2|*", cluster.getRootGroup().getPartitions().get()); // Reduced from 4|4|*
+ assertEquals(0, cluster.getRootGroup().getNodes().size());
+ assertEquals(3, cluster.getRootGroup().getSubgroups().size());
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getIndex(), is("0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().size(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(0).getConfigId(), is("bar/storage/0"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getDistributionKey(), is(1));
+ assertThat(cluster.getRootGroup().getSubgroups().get(0).getNodes().get(1).getConfigId(), is("bar/storage/1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getIndex(), is("1"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().size(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getDistributionKey(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(0).getConfigId(), is("bar/storage/2"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getDistributionKey(), is(3));
+ assertThat(cluster.getRootGroup().getSubgroups().get(1).getNodes().get(1).getConfigId(), is("bar/storage/3"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(2).getIndex(), is("2"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(2).getNodes().size(), is(2));
+ assertThat(cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getDistributionKey(), is(4));
+ assertThat(cluster.getRootGroup().getSubgroups().get(2).getNodes().get(0).getConfigId(), is("bar/storage/4"));
+ assertThat(cluster.getRootGroup().getSubgroups().get(2).getNodes().get(1).getDistributionKey(), is(5));
+ assertThat(cluster.getRootGroup().getSubgroups().get(2).getNodes().get(1).getConfigId(), is("bar/storage/5"));
+ }
+
+ @Test
+ public void testUsingNodesCountAttributesAndGettingTooFewNodes() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services>" +
+ " <admin version='3.0'>" +
+ " <nodes count='3'/>" + // Ignored
+ " </admin>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy reply-after='8'>12</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='24'/>" +
+ " <engine><proton><searchable-copies>5</searchable-copies></proton></engine>" +
+ " <dispatch><num-dispatch-groups>7</num-dispatch-groups></dispatch>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 4;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, false);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ assertEquals(4, cluster.redundancy().effectiveInitialRedundancy());
+ assertEquals(4, cluster.redundancy().effectiveFinalRedundancy());
+ assertEquals(4, cluster.redundancy().effectiveReadyCopies());
+ assertEquals(4, cluster.getSearch().getIndexed().getDispatchSpec().getGroups().size());
+ assertFalse(cluster.getRootGroup().getPartitions().isPresent());
+ assertEquals(4, cluster.getRootGroup().getNodes().size());
+ assertEquals(0, cluster.getRootGroup().getSubgroups().size());
+ assertThat(cluster.getRootGroup().getNodes().size(), is(4));
+ assertThat(cluster.getRootGroup().getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getNodes().get(0).getConfigId(), is("bar/storage/0"));
+ assertThat(cluster.getRootGroup().getNodes().get(1).getDistributionKey(), is(1));
+ assertThat(cluster.getRootGroup().getNodes().get(1).getConfigId(), is("bar/storage/1"));
+ assertThat(cluster.getRootGroup().getNodes().get(2).getDistributionKey(), is(2));
+ assertThat(cluster.getRootGroup().getNodes().get(2).getConfigId(), is("bar/storage/2"));
+ assertThat(cluster.getRootGroup().getNodes().get(3).getDistributionKey(), is(3));
+ assertThat(cluster.getRootGroup().getNodes().get(3).getConfigId(), is("bar/storage/3"));
+ }
+
+ @Test
+ public void testUsingNodesAndGroupCountAttributesAndGettingJustOneNode() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='3.0'>" +
+ " <nodes count='3'/>" + // Ignored
+ " </admin>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy reply-after='3'>4</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='24' groups='3'/>" +
+ " <engine><proton><searchable-copies>3</searchable-copies></proton></engine>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 1; // We only have 1 content node -> 1 groups with redundancy 1
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, false);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ ContainerCluster clusterControllers = cluster.getClusterControllers();
+ assertEquals(1, clusterControllers.getContainers().size());
+ assertEquals("bar-controllers", clusterControllers.getName());
+ assertEquals("foo0", clusterControllers.getContainers().get(0).getHostName());
+ assertEquals(1, cluster.redundancy().effectiveInitialRedundancy()); // Reduced from 3*3
+ assertEquals(1, cluster.redundancy().effectiveFinalRedundancy()); // Reduced from 3*4
+ assertEquals(1, cluster.redundancy().effectiveReadyCopies()); // Reduced from 3*3
+ assertFalse(cluster.getRootGroup().getPartitions().isPresent()); // 1 group - > flattened -> no distribution
+ assertEquals(1, cluster.getRootGroup().getNodes().size());
+ assertEquals(0, cluster.getRootGroup().getSubgroups().size());
+ assertThat(cluster.getRootGroup().getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getNodes().get(0).getConfigId(), is("bar/storage/0"));
+ }
+
+ @Test
+ public void testUsingNodesCountAttributesAndGettingJustOneNode() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='3.0'>" +
+ " <nodes count='3'/>" + // Ignored
+ " </admin>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy reply-after='8'>12</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='24'/>" +
+ " <engine><proton><searchable-copies>5</searchable-copies></proton></engine>" +
+ " <dispatch><num-dispatch-groups>7</num-dispatch-groups></dispatch>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 1;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, false);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ ContentCluster cluster = model.getContentClusters().get("bar");
+ assertEquals(1, cluster.redundancy().effectiveInitialRedundancy());
+ assertEquals(1, cluster.redundancy().effectiveFinalRedundancy());
+ assertEquals(1, cluster.redundancy().effectiveReadyCopies());
+
+ assertEquals(1, cluster.getSearch().getIndexed().getDispatchSpec().getGroups().size());
+ assertFalse(cluster.getRootGroup().getPartitions().isPresent());
+ assertEquals(1, cluster.getRootGroup().getNodes().size());
+ assertEquals(0, cluster.getRootGroup().getSubgroups().size());
+ assertThat(cluster.getRootGroup().getNodes().size(), is(1));
+ assertThat(cluster.getRootGroup().getNodes().get(0).getDistributionKey(), is(0));
+ assertThat(cluster.getRootGroup().getNodes().get(0).getConfigId(), is("bar/storage/0"));
+ }
+
+ @Test
+ public void testRequestingSpecificFlavors() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'>" +
+ " <logservers><nodes count='1' dedicated='true' flavor='logserver-flavor'/></logservers>" +
+ " <slobroks><nodes count='2' dedicated='true' flavor='slobrok-flavor'/></slobroks>" +
+ " </admin>" +
+ " <container version='1.0' id='container'>" +
+ " <nodes count='4' flavor='container-flavor'/>" +
+ " </container>" +
+ " <content version='1.0' id='foo'>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <controllers><nodes count='2' dedicated='true' flavor='controller-foo-flavor'/></controllers>" +
+ " <nodes count='5' flavor='content-foo-flavor'/>" +
+ " </content>" +
+ " <content version='1.0' id='bar'>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <controllers><nodes count='3' dedicated='true' flavor='controller-bar-flavor'/></controllers>" +
+ " <nodes count='6' flavor='content-bar-flavor'/>" +
+ " </content>" +
+ "</services>";
+
+ int totalHosts = 23;
+ Map<String, Collection<Host>> hosts = new HashMap<>();
+ hosts.put("logserver-flavor", createHosts("logserver-flavor", 1).getHosts());
+ hosts.put("slobrok-flavor", createHosts("slobrok-flavor", 2).getHosts());
+ hosts.put("container-flavor", createHosts("container-flavor", 4).getHosts());
+ hosts.put("controller-foo-flavor", createHosts("controller-foo-flavor", 2).getHosts());
+ hosts.put("content-foo-flavor", createHosts("content-foo-flavor", 5).getHosts());
+ hosts.put("controller-bar-flavor", createHosts("controller-bar-flavor", 3).getHosts());
+ hosts.put("content-bar-flavor", createHosts("content-bar-flavor", 6).getHosts());
+ VespaModel model = createModel(services, hosts, true, 0); // fails unless the right flavors+counts are requested
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(totalHosts));
+ }
+
+ @Test
+ public void testJDiscOnly() throws Exception {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<jdisc version='1.0'>" +
+ " <search/>" +
+ " <nodes count='3'/>" +
+ "</jdisc>";
+ int numberOfHosts = 3;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertEquals(numberOfHosts, model.getRoot().getHostSystem().getHosts().size());
+ assertEquals(3, model.getContainerClusters().get("jdisc").getContainers().size());
+ assertNotNull(model.getAdmin().getLogserver());
+ assertEquals(3, model.getAdmin().getSlobroks().size());
+ }
+
+ @Test
+ public void testUsingHostaliasWithProvisioner() throws Exception {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>\n" +
+ "\n" +
+ "<admin version='2.0'>" +
+ " <adminserver hostalias='node1'/>\n"+
+ "</admin>\n" +
+ "<jdisc id='mydisc' version='1.0'>" +
+ " <handler id='myHandler'>" +
+ " <component id='injected' />" +
+ " </handler>" +
+ " <nodes>" +
+ " <node hostalias='node1'/>" +
+ " </nodes>" +
+ "</jdisc>" +
+ "</services>";
+ int numberOfHosts = 1;
+ Hosts hosts = createHosts(numberOfHosts);
+ VespaModel model = createModel(services, hosts, true);
+ assertEquals(1, model.getRoot().getHostSystem().getHosts().size());
+ assertEquals(1, model.getAdmin().getSlobroks().size());
+ }
+
+ @Test
+ public void testThatStandaloneSyntaxWorksOnHostedVespa() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<jdisc id='foo' version='1.0'>" +
+ " <http>" +
+ " <server id='server1' port='" + Defaults.getDefaults().vespaWebServicePort() + "' />" +
+ " </http>" +
+ "</jdisc>";
+ Hosts hosts = createHosts(1);
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getHosts().size(), is(1));
+ assertThat(model.getContainerClusters().size(), is(1));
+ }
+
+ /** Recreate the combination used in some factory tests */
+ @Test
+ public void testMultitenantButNotHosted() throws Exception {
+ String services =
+ "<?xml version='1.0' encoding='UTF-8' ?>" +
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node1'/>" +
+ " </admin>" +
+ " <jdisc id='default' version='1.0'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='node1'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content id='storage' version='1.0'>" +
+ " <redundancy>2</redundancy>" +
+ " <group>" +
+ " <node distribution-key='0' hostalias='node1'/>" +
+ " <node distribution-key='1' hostalias='node1'/>" +
+ " </group>" +
+ " <tuning>" +
+ " <cluster-controller>" +
+ " <transition-time>0</transition-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " <documents>" +
+ " <document mode='store-only' type='type1'/>" +
+ " </documents>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " </content>" +
+ " </services>";
+
+ VespaModel model = createNonProvisionedMultitenantModel(services);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(1));
+ ContentCluster content = model.getContentClusters().get("storage");
+ assertEquals(2, content.getRootGroup().getNodes().size());
+ ContainerCluster controller = content.getClusterControllers();
+ assertEquals(1, controller.getContainers().size());
+ }
+
+ @Test
+ public void testMultitenantButNotHostedSharedContentNode() throws Exception {
+ String services =
+ "<?xml version='1.0' encoding='UTF-8' ?>" +
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node1'/>" +
+ " </admin>" +
+ " <jdisc id='default' version='1.0'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='node1'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content id='storage' version='1.0'>" +
+ " <redundancy>2</redundancy>" +
+ " <group>" +
+ " <node distribution-key='0' hostalias='node1'/>" +
+ " <node distribution-key='1' hostalias='node1'/>" +
+ " </group>" +
+ " <tuning>" +
+ " <cluster-controller>" +
+ " <transition-time>0</transition-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " <documents>" +
+ " <document mode='store-only' type='type1'/>" +
+ " </documents>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " </content>" +
+ " <content id='search' version='1.0'>" +
+ " <redundancy>2</redundancy>" +
+ " <group>" +
+ " <node distribution-key='0' hostalias='node1'/>" +
+ " </group>" +
+ " <documents>" +
+ " <document type='type1'/>" +
+ " </documents>" +
+ " </content>" +
+ " </services>";
+
+ VespaModel model = createNonProvisionedMultitenantModel(services);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(1));
+ ContentCluster content = model.getContentClusters().get("storage");
+ assertEquals(2, content.getRootGroup().getNodes().size());
+ ContainerCluster controller = content.getClusterControllers();
+ assertEquals(1, controller.getContainers().size());
+ }
+
+ private VespaModel createNonProvisionedMultitenantModel(String services) throws ParseException {
+ final VespaModelCreatorWithMockPkg modelCreatorWithMockPkg = new VespaModelCreatorWithMockPkg(null, services, ApplicationPackageUtils.generateSearchDefinition("type1"));
+ final ApplicationPackage appPkg = modelCreatorWithMockPkg.appPkg;
+ DeployState deployState = new DeployState.Builder().applicationPackage(appPkg).
+ properties((new DeployProperties.Builder()).multitenant(true).build()).
+ build();
+ return modelCreatorWithMockPkg.create(false, deployState);
+ }
+
+ @Test
+ public void testThatTldConfigIdsAreDeterministic() throws ParseException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services>" +
+ " <admin version='4.0'/>" +
+ " <jdisc version='1.0' id='jdisc0'>" +
+ " <search/>" +
+ " <nodes count='2'/>" +
+ " </jdisc>" +
+ " <jdisc version='1.0' id='jdisc1'>" +
+ " <search/>" +
+ " <nodes count='2'/>" +
+ " </jdisc>" +
+ " <content version='1.0' id='content0'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='2'/>" +
+ " </content>" +
+ " <content version='1.0' id='content1'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='type1' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='2'/>" +
+ " </content>" +
+ "</services>";
+
+ int numberOfHosts = 8;
+
+ {
+ Hosts hosts = createHosts(numberOfHosts);
+ // Nodes used will be foo0, foo1, .. and so on.
+ VespaModel model = createModel(services, hosts, true);
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ final Map<String, ContentCluster> contentClusters = model.getContentClusters();
+ assertEquals(2, contentClusters.size());
+
+ checkThatTldAndContainerRunningOnSameHostHaveSameId(
+ model.getContainerClusters().values(),
+ model.getContentClusters().values(),
+ 0);
+ }
+
+ {
+ Hosts hosts = createHosts(numberOfHosts + 1);
+ // Start numbering nodes with index 1 and retire first node
+ // Nodes used will be foo1, foo2, .. and so on. Containers will start with index 1, not 0 as they are in the test above
+ VespaModel model = createModel(services, Collections.singletonMap("default", hosts.getHosts()), true, 1, "foo0");
+ assertThat(model.getRoot().getHostSystem().getHosts().size(), is(numberOfHosts));
+
+ final Map<String, ContentCluster> contentClusters = model.getContentClusters();
+ assertEquals(2, contentClusters.size());
+
+ checkThatTldAndContainerRunningOnSameHostHaveSameId(
+ model.getContainerClusters().values(),
+ model.getContentClusters().values(),
+ 1);
+ }
+ }
+
+ private void checkThatTldAndContainerRunningOnSameHostHaveSameId(Collection<ContainerCluster> containerClusters,
+ Collection<ContentCluster> contentClusters,
+ int startIndexForContainerIds) {
+ for (ContentCluster contentCluster : contentClusters) {
+ final String contentClusterName = contentCluster.getName();
+ int i = 0;
+ for (ContainerCluster containerCluster : containerClusters) {
+ final String containerClusterName = containerCluster.getName();
+ for (int j = 0; j < 2; j++) {
+ final Dispatch tld = contentCluster.getSearch().getIndexed().getTLDs().get(2 * i + j);
+ final Container container = containerCluster.getContainers().get(j);
+ final int containerConfigIdIndex = j + startIndexForContainerIds;
+
+ assertEquals(container.getHostName(), tld.getHostname());
+ assertEquals(contentClusterName + "/search/cluster." + contentClusterName + "/tlds/" +
+ containerClusterName + "." + containerConfigIdIndex + ".tld." + containerConfigIdIndex,
+ tld.getConfigId());
+ assertEquals(containerClusterName + "/" + "container." + containerConfigIdIndex,
+ container.getConfigId());
+ }
+ i++;
+ }
+ }
+ }
+
+ private void assertIllegalModel(String services, String expectedIllegalTag) {
+ try {
+ createModel(services, createHosts(2), true);
+ fail("Expected that test failed");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is(expectedIllegalTag + " is not allowed when running Vespa in a hosted environment"));
+ } catch (ParseException e) {
+ fail("Test failed unexpectedly: " + e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/provision/SingleNodeProvisionerTest.java b/config-model/src/test/java/com/yahoo/config/model/provision/SingleNodeProvisionerTest.java
new file mode 100644
index 00000000000..e48baaadfd0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/provision/SingleNodeProvisionerTest.java
@@ -0,0 +1,99 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.provision;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.HostSpec;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.*;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author musum
+ */
+public class SingleNodeProvisionerTest {
+ @Test
+ public void require_basic_works() {
+ SingleNodeProvisioner hostProvisioner = new SingleNodeProvisioner();
+
+ // 4 services, 2 host aliases, mapping to 2 host.
+ List<String> aliases = createAliases();
+ Map<String, HostSpec> map = allocate(hostProvisioner, aliases);
+ assertCorrectNumberOfHost(map, 1);
+ assertThat(map.size(), is(2));
+ assertTrue(map.keySet().containsAll(aliases));
+
+ // 5 services, 3 host aliases, mapping to 2 host.
+ aliases = createAliases(Collections.singletonList("node3"));
+ map = allocate(hostProvisioner, aliases);
+
+ assertCorrectNumberOfHost(map, 1);
+ assertThat(map.size(), is(3));
+ assertTrue(map.keySet().containsAll(aliases));
+
+ // 5 services, 3 host aliases, mapping to 3 host.
+ aliases = createAliases(Collections.singletonList("node4"));
+ map = allocate(hostProvisioner, aliases);
+ assertThat(map.size(), is(3));
+ assertCorrectNumberOfHost(map, 1);
+ assertTrue(map.keySet().containsAll(aliases));
+ }
+
+ @Test
+ public void require_allocate_clustermembership_works() throws IOException, SAXException {
+ String servicesXml = "<services version='1.0'>"
+ + " <admin version='3.0'>"
+ + " <nodes count='1' />"
+ + " </admin>"
+ + " <jdisc version='1.0'>"
+ + " <search />"
+ + " <nodes count='1' />"
+ + " </jdisc>"
+ + "</services>";
+ ApplicationPackage app = new MockApplicationPackage.Builder().withServices(servicesXml).build();
+ VespaModel model = new VespaModel(app);
+ assertThat(model.getHosts().size(), is(1));
+ }
+
+
+ private Map<String, HostSpec> allocate(HostProvisioner provisioner, List<String> aliases) {
+ Map<String, HostSpec> map = new LinkedHashMap<>();
+ for (String alias : aliases) {
+ map.put(alias, provisioner.allocateHost(alias));
+ }
+ return map;
+ }
+
+
+ private void assertCorrectNumberOfHost(Map<String, HostSpec> hostToServiceMap, int expectedHostCount) {
+ Set<String> hostSet = new HashSet<>();
+ for (HostSpec host : hostToServiceMap.values()) {
+ hostSet.add(host.hostname());
+ }
+ assertThat(hostSet.size(), is(expectedHostCount));
+ }
+
+ private List<String> createAliases() {
+ return createAliases(new ArrayList<String>());
+ }
+
+ // Admin services on node1, qrserver on node2 + additional specs
+ private List<String> createAliases(Collection<String> additionalAliases) {
+ List<String> aliases = new ArrayList<>();
+ aliases.add("node1");
+ aliases.add("node1");
+ aliases.add("node1");
+ aliases.add("node2");
+ aliases.addAll(additionalAliases);
+ return aliases;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/config/model/test/MockHosts.java b/config-model/src/test/java/com/yahoo/config/model/test/MockHosts.java
new file mode 100644
index 00000000000..d68e67835d2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/config/model/test/MockHosts.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.test;
+
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.SimpleConfigProducer;
+
+/**
+ * @author tonytv
+ */
+public class MockHosts {
+
+ private final MockRoot root = new MockRoot();
+ private final SimpleConfigProducer<Host> hosts = new SimpleConfigProducer<>(root, "hosts");
+
+ public final Host host1 = new Host(hosts, "host-01.example.yahoo.com");
+ public final Host host2 = new Host(hosts, "host-02.example.yahoo.com");
+ public final Host host3 = new Host(hosts, "host-03.example.yahoo.com");
+
+}
diff --git a/config-model/src/test/java/com/yahoo/document/test/SDDocumentTypeTestCase.java b/config-model/src/test/java/com/yahoo/document/test/SDDocumentTypeTestCase.java
new file mode 100644
index 00000000000..1250ac45916
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/document/test/SDDocumentTypeTestCase.java
@@ -0,0 +1,129 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.test;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.DataTypeName;
+import com.yahoo.documentmodel.VespaDocumentType;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+
+/**
+ TODO: Document purpose
+
+ @author <a href="thomasg@yahoo-inc.com>Thomas Gundersen</a>
+ @author <a href="bratseth@yahoo-inc.com>Jon S Bratseth</a>
+*/
+public class SDDocumentTypeTestCase extends SearchDefinitionTestCase {
+
+ // Verify that we can register and retrieve fields.
+ @Test
+ public void testSetGet() {
+ SDDocumentType docType=new SDDocumentType("testdoc");
+ docType.addField("Bongle",DataType.STRING);
+ docType.addField("nalle",DataType.INT);
+
+ assertNotNull(docType.getField("Bongle").getName(),"Bongle");
+ assertNull(docType.getField("bongle"));
+
+ }
+ @Test
+ public void testInheritance() {
+
+ SDDocumentType child=new SDDocumentType("child");
+ Iterator<SDDocumentType> inherited=child.getInheritedTypes().iterator();
+ assertTrue(inherited.hasNext());
+ assertEquals(inherited.next().getDocumentName(), VespaDocumentType.NAME);
+ assertFalse(inherited.hasNext());
+
+ child.addField("childfield",DataType.INT);
+ SDField overridden= child.addField("overridden", DataType.STRING);
+
+ SDDocumentType parent1=new SDDocumentType("parent1");
+ SDField overridden2= parent1.addField("overridden", DataType.STRING);
+ parent1.addField("parent1field",DataType.STRING);
+ child.inherit(parent1);
+
+ SDDocumentType parent2=new SDDocumentType("parent2");
+ parent2.addField("parent2field",DataType.STRING);
+ child.inherit(parent2);
+
+ SDDocumentType root=new SDDocumentType("root");
+ root.addField("rootfield",DataType.STRING);
+ parent1.inherit(root);
+ parent2.inherit(root);
+
+ inherited=child.getInheritedTypes().iterator();
+ assertEquals(VespaDocumentType.NAME,inherited.next().getDocumentName());
+ assertEquals(new DataTypeName("parent1"),inherited.next().getDocumentName());
+ assertEquals(new DataTypeName("parent2"),inherited.next().getDocumentName());
+ assertTrue(!inherited.hasNext());
+
+ inherited=parent1.getInheritedTypes().iterator();
+ assertEquals(VespaDocumentType.NAME,inherited.next().getDocumentName());
+ assertEquals(new DataTypeName("root"),inherited.next().getDocumentName());
+ assertTrue(!inherited.hasNext());
+
+ inherited=parent2.getInheritedTypes().iterator();
+ assertEquals(VespaDocumentType.NAME,inherited.next().getDocumentName());
+ assertEquals(new DataTypeName("root"),inherited.next().getDocumentName());
+ assertTrue(!inherited.hasNext());
+
+ inherited=root.getInheritedTypes().iterator();
+ assertTrue(inherited.hasNext());
+ assertEquals(inherited.next().getDocumentName(), VespaDocumentType.NAME);
+ assertFalse(inherited.hasNext());
+
+
+ Iterator fields=child.fieldSet().iterator();
+ SDField field;
+
+ field=(SDField)fields.next();
+ assertEquals("rootfield",field.getName());
+
+ field=(SDField)fields.next();
+ assertEquals("overridden",field.getName());
+
+ field=(SDField)fields.next();
+ assertEquals("parent1field",field.getName());
+
+ field=(SDField)fields.next();
+ assertEquals("parent2field",field.getName());
+
+ field=(SDField)fields.next();
+ assertEquals("childfield",field.getName());
+
+ // TODO: Test uninheriting
+ }
+ /* What is this?.. DocumentTypeIds aren't used for anything as far as I can see, and is now ignored by document, H\u00F9kon
+ public void testId() {
+ Search search = new Search("cocacola");
+ SDDocumentType sugar = new SDDocumentType("sugar", 3, true, new DocumentTypeId(5), search);
+ search.addDocument(sugar);
+ try {
+ SDDocumentType color = new SDDocumentType("color", 2, true, new DocumentTypeId(5), search);
+ fail();
+ } catch (RuntimeException re) {
+ }
+
+ SDDocumentType taste = new SDDocumentType("taste", 3, true, search);
+ search.addDocument(taste);
+ try {
+ SDDocumentType secondtaste = new SDDocumentType("taste", 3, true, search);
+ fail();
+ } catch (RuntimeException re) {
+ }
+
+ SDDocumentType goodtaste = new SDDocumentType("goodtaste", 3, true, search);
+ search.addDocument(taste);
+ SDDocumentType badtaste = new SDDocumentType("badtaste", 3, true, search);
+ search.addDocument(taste);
+ }
+ */
+
+}
diff --git a/config-model/src/test/java/com/yahoo/document/test/SDFieldTestCase.java b/config-model/src/test/java/com/yahoo/document/test/SDFieldTestCase.java
new file mode 100644
index 00000000000..ec8639d1b4d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/document/test/SDFieldTestCase.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.test;
+
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+/**
+ @author <a href="thomasg@yahoo-inc.com>Thomas Gundersen</a>
+*/
+public class SDFieldTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testIdSettingConflict() {
+ SDDocumentType doc=new SDDocumentType("testdoc");
+ SDField one=(SDField) doc.addField("one", DataType.STRING, false, 60);
+
+ SDField two=(SDField) doc.addField("two", DataType.STRING, false, 61);
+
+ try {
+ SDField three=(SDField) doc.addField("three", DataType.STRING, false, 60);
+ fail("Allowed to set duplicate id");
+ }
+ catch (IllegalArgumentException e) {
+ // Success
+ }
+ }
+ @Test
+ public void testSettingReservedId() {
+ SDDocumentType doc=new SDDocumentType("testdoc");
+ try {
+ SDField one=(SDField) doc.addField("one", DataType.STRING, false, 127);
+ fail("Allowed to set reserved id");
+ }
+ catch (IllegalArgumentException e) {
+ // Success
+ }
+
+ try {
+ SDField one=(SDField) doc.addField("one", DataType.STRING, false, 100);
+ fail("Allowed to set reserved id");
+ }
+ catch (IllegalArgumentException e) {
+ // Success
+ }
+
+ try {
+ SDField one=(SDField) doc.addField("one", DataType.STRING, false, -1);
+ fail("Allowed to set reserved id");
+ }
+ catch (IllegalArgumentException e) {
+ // Success
+ }
+ SDField one= doc.addField("one", DataType.STRING);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/ArraysTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/ArraysTestCase.java
new file mode 100644
index 00000000000..210f2d92ac0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/ArraysTestCase.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.ArrayDataType;
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+/**
+ * tests importing of document containing array type fields
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ArraysTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testArrayImporting() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/arrays.sd");
+
+ SDField tags = (SDField)search.getDocument().getField("tags");
+ assertEquals(DataType.STRING, ((CollectionDataType)tags.getDataType()).getNestedType());
+
+ SDField ratings = (SDField)search.getDocument().getField("ratings");
+ assertTrue(ratings.getDataType() instanceof ArrayDataType);
+ assertEquals(DataType.INT, ((ArrayDataType)ratings.getDataType()).getNestedType());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/ArraysWeightedSetsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/ArraysWeightedSetsTestCase.java
new file mode 100644
index 00000000000..e9d37a731e7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/ArraysWeightedSetsTestCase.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.ArrayDataType;
+import com.yahoo.document.CollectionDataType;
+import com.yahoo.document.DataType;
+import com.yahoo.document.WeightedSetDataType;
+import com.yahoo.searchdefinition.document.SDField;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * tests importing of document containing array type fields and weighted set type fields, new syntax.
+ *
+ * @author <a href="einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class ArraysWeightedSetsTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testArrayWeightedSetsImporting() throws java.io.IOException, com.yahoo.searchdefinition.parser.ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/arraysweightedsets.sd");
+
+ SDField tags = (SDField) search.getDocument().getField("tags");
+ assertTrue(tags.getDataType() instanceof ArrayDataType);
+ assertEquals(DataType.STRING, ((CollectionDataType)tags.getDataType()).getNestedType());
+
+ SDField ratings = (SDField) search.getDocument().getField("ratings");
+ assertTrue(ratings.getDataType() instanceof ArrayDataType);
+ assertEquals(DataType.INT, ((CollectionDataType)ratings.getDataType()).getNestedType());
+
+ SDField flags = (SDField) search.getDocument().getField("flags");
+ assertTrue(flags.getDataType() instanceof WeightedSetDataType);
+ assertEquals(DataType.STRING, ((CollectionDataType)flags.getDataType()).getNestedType());
+
+ SDField banners = (SDField) search.getDocument().getField("banners");
+ assertTrue(banners.getDataType() instanceof WeightedSetDataType);
+ assertEquals(DataType.INT, ((CollectionDataType)banners.getDataType()).getNestedType());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/AttributeSettingsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/AttributeSettingsTestCase.java
new file mode 100644
index 00000000000..ddffe6d6f48
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/AttributeSettingsTestCase.java
@@ -0,0 +1,91 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * Attribute settings
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class AttributeSettingsTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testAttributeSettings() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/attributesettings.sd");
+
+ SDField f1=(SDField) search.getDocument().getField("f1");
+ assertTrue(f1.getAttributes().size() == 1);
+ Attribute a1 = f1.getAttributes().get(f1.getName());
+ assertThat(a1.getType(), is(Attribute.Type.LONG));
+ assertThat(a1.getCollectionType(), is(Attribute.CollectionType.SINGLE));
+ assertTrue(a1.isHuge());
+ assertFalse(a1.isFastSearch());
+ assertFalse(a1.isFastAccess());
+ assertFalse(a1.isRemoveIfZero());
+ assertFalse(a1.isCreateIfNonExistent());
+
+ SDField f2=(SDField) search.getDocument().getField("f2");
+ assertTrue(f2.getAttributes().size() == 1);
+ Attribute a2 = f2.getAttributes().get(f2.getName());
+ assertThat(a2.getType(), is(Attribute.Type.LONG));
+ assertThat(a2.getCollectionType(), is(Attribute.CollectionType.SINGLE));
+ assertFalse(a2.isHuge());
+ assertTrue(a2.isFastSearch());
+ assertFalse(a2.isFastAccess());
+ assertFalse(a2.isRemoveIfZero());
+ assertFalse(a2.isCreateIfNonExistent());
+ assertThat(f2.getAliasToName().get("f2alias"), is("f2"));
+ SDField f3=(SDField) search.getDocument().getField("f3");
+ assertTrue(f3.getAttributes().size() == 1);
+ assertThat(f3.getAliasToName().get("f3alias"), is("f3"));
+
+ Attribute a3 = f3.getAttributes().get(f3.getName());
+ assertThat(a3.getType(), is(Attribute.Type.LONG));
+ assertThat(a3.getCollectionType(), is(Attribute.CollectionType.SINGLE));
+ assertFalse(a3.isHuge());
+ assertFalse(a3.isFastSearch());
+ assertFalse(a3.isFastAccess());
+ assertFalse(a3.isRemoveIfZero());
+ assertFalse(a3.isCreateIfNonExistent());
+
+ assertWeightedSet(search,"f4",true,true);
+ assertWeightedSet(search,"f5",true,true);
+ assertWeightedSet(search,"f6",true,true);
+ assertWeightedSet(search,"f7",true,false);
+ assertWeightedSet(search,"f8",true,false);
+ assertWeightedSet(search,"f9",false,true);
+ assertWeightedSet(search,"f10",false,true);
+ }
+
+ private void assertWeightedSet(Search search, String name, boolean createIfNonExistent, boolean removeIfZero) {
+ SDField f4 = (SDField) search.getDocument().getField(name);
+ assertTrue(f4.getAttributes().size() == 1);
+ Attribute a4 = f4.getAttributes().get(f4.getName());
+ assertThat(a4.getType(), is(Attribute.Type.STRING));
+ assertThat(a4.getCollectionType(), is(Attribute.CollectionType.WEIGHTEDSET));
+ assertFalse(a4.isHuge());
+ assertFalse(a4.isFastSearch());
+ assertFalse(a4.isFastAccess());
+ assertThat(removeIfZero, is(a4.isRemoveIfZero()));
+ assertThat(createIfNonExistent, is(a4.isCreateIfNonExistent()));
+ }
+
+ @Test
+ public void requireThatFastAccessCanBeSet() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/attributesettings.sd");
+ SDField field = (SDField) search.getDocument().getField("fast_access");
+ assertTrue(field.getAttributes().size() == 1);
+ Attribute attr = field.getAttributes().get(field.getName());
+ assertTrue(attr.isFastAccess());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/CommentTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/CommentTestCase.java
new file mode 100644
index 00000000000..26b6444ec3a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/CommentTestCase.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests comment handling
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class CommentTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testComments() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/comment.sd");
+ SDField field = search.getField("a");
+ assertEquals("{ input a | tokenize normalize stem:\"SHORTEST\" | summary a | index a; }",
+ field.getIndexingScript().toString());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/DiversityTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/DiversityTestCase.java
new file mode 100644
index 00000000000..6aebe765bc4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/DiversityTestCase.java
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.search.query.ranking.Diversity;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+import static org.junit.Assert.fail;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Created by balder on 3/10/15.
+ */
+public class DiversityTestCase {
+ @Test
+ public void testDiversity() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type int { \n" +
+ " indexing: attribute \n" +
+ " attribute: fast-search\n" +
+ " }\n" +
+ " field b type int {\n" +
+ " indexing: attribute \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " match-phase {\n" +
+ " diversity {\n" +
+ " attribute: b\n" +
+ " min-groups: 74\n" +
+ " cutoff-factor: 17.3\n" +
+ " cutoff-strategy: strict" +
+ " }\n" +
+ " attribute: a\n" +
+ " max-hits: 120\n" +
+ " max-filter-coverage: 0.065" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+ RankProfile.MatchPhaseSettings matchPhase = rankProfileRegistry.getRankProfile(s, "parent").getMatchPhaseSettings();
+ RankProfile.DiversitySettings diversity = matchPhase.getDiversity();
+ assertEquals("b", diversity.getAttribute());
+ assertEquals(74, diversity.getMinGroups());
+ assertEquals(17.3, diversity.getCutoffFactor(), 1e-16);
+ assertEquals(Diversity.CutoffStrategy.strict, diversity.getCutoffStrategy());
+ assertEquals(120, matchPhase.getMaxHits());
+ assertEquals("a", matchPhase.getAttribute());
+ assertEquals(0.065, matchPhase.getMaxFilterCoverage(), 1e-16);
+ }
+
+ private static String getMessagePrefix() {
+ return "In search definition 'test', rank-profile 'parent': diversity attribute 'b' ";
+ }
+ @Test
+ public void requireSingleNumericOrString() throws ParseException {
+ SearchBuilder builder = getSearchBuilder("field b type predicate { indexing: attribute }");
+
+ try {
+ builder.build();
+ fail("Should throw.");
+ } catch (IllegalArgumentException e) {
+ assertEquals(getMessagePrefix() + "must be single value numeric, or enumerated attribute, but it is 'predicate'", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireSingle() throws ParseException {
+ SearchBuilder builder = getSearchBuilder("field b type array<int> { indexing: attribute }");
+
+ try {
+ builder.build();
+ fail("Should throw.");
+ } catch (IllegalArgumentException e) {
+ assertEquals(getMessagePrefix() + "must be single value numeric, or enumerated attribute, but it is 'Array<int>'", e.getMessage());
+ }
+ }
+ private SearchBuilder getSearchBuilder(String diversity) throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type int { \n" +
+ " indexing: attribute \n" +
+ " attribute: fast-search\n" +
+ " }\n" +
+ diversity +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " match-phase {\n" +
+ " diversity {\n" +
+ " attribute: b\n" +
+ " min-groups: 74\n" +
+ " }\n" +
+ " attribute: a\n" +
+ " max-hits: 120\n" +
+ " }\n" +
+ " }\n" +
+ "}\n");
+ return builder;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/FieldOfTypeDocumentTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/FieldOfTypeDocumentTestCase.java
new file mode 100644
index 00000000000..4a3119e55b7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/FieldOfTypeDocumentTestCase.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.*;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.searchdefinition.derived.Deriver;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class FieldOfTypeDocumentTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testDocument() throws IOException, ParseException {
+
+ List<String> sds = new ArrayList<>();
+ sds.add("src/test/examples/music.sd");
+ sds.add("src/test/examples/fieldoftypedocument.sd");
+ DocumentmanagerConfig.Builder value = Deriver.getDocumentManagerConfig(sds);
+ assertConfigFile("src/test/examples/fieldoftypedocument.cfg",
+ new DocumentmanagerConfig(value).toString() + "\n");
+
+ DocumentTypeManager manager = new DocumentTypeManager();
+ DocumentTypeManagerConfigurer.configure(manager, "raw:" + new DocumentmanagerConfig(value).toString());
+
+
+ DocumentType musicType = manager.getDocumentType("music");
+ assertEquals(5, musicType.getFieldCount());
+
+ Field intField = musicType.getField("intfield");
+ assertEquals(DataType.INT, intField.getDataType());
+ Field stringField = musicType.getField("stringfield");
+ assertEquals(DataType.STRING, stringField.getDataType());
+ Field longField = musicType.getField("longfield");
+ assertEquals(DataType.LONG, longField.getDataType());
+ Field summaryfeatures = musicType.getField("summaryfeatures");
+ assertEquals(DataType.STRING, summaryfeatures.getDataType());
+ Field rankfeatures = musicType.getField("rankfeatures");
+ assertEquals(DataType.STRING, rankfeatures.getDataType());
+
+
+ DocumentType bookType = manager.getDocumentType("book");
+ assertEquals(3, bookType.getFieldCount());
+
+ Field musicField = bookType.getField("soundtrack");
+ assertSame(musicType, musicField.getDataType());
+ summaryfeatures = musicType.getField("summaryfeatures");
+ assertEquals(DataType.STRING, summaryfeatures.getDataType());
+ rankfeatures = musicType.getField("rankfeatures");
+ assertEquals(DataType.STRING, rankfeatures.getDataType());
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/IncorrectRankingExpressionFileRefTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/IncorrectRankingExpressionFileRefTestCase.java
new file mode 100644
index 00000000000..150a759ecf4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/IncorrectRankingExpressionFileRefTestCase.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class IncorrectRankingExpressionFileRefTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testIncorrectRef() throws IOException, ParseException {
+ try {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ Search search = SearchBuilder.buildFromFile("src/test/examples/incorrectrankingexpressionfileref.sd", registry);
+ new DerivedConfiguration(search, registry); // rank profile parsing happens during deriving
+ fail("parsing should have failed");
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ assertTrue(e.getCause().getMessage().contains("Could not read ranking expression file"));
+ assertTrue(e.getCause().getMessage().contains("wrongending.expr.expression"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/IncorrectSummaryTypesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/IncorrectSummaryTypesTestCase.java
new file mode 100644
index 00000000000..b6e3dc1b442
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/IncorrectSummaryTypesTestCase.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+/**
+ * Tests importing a search definition with conflicting summary types
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class IncorrectSummaryTypesTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testImportingIncorrect() throws IOException, ParseException {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/incorrectsummarytypes.sd");
+ fail("processing should have failed");
+ } catch (RuntimeException e) {
+ assertEquals("'summary somestring type string' in 'destinations(default )' is inconsistent with 'summary somestring type int' in 'destinations(incorrect )': All declarations of the same summary field must have the same type", e.getMessage());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/IndexSettingsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/IndexSettingsTestCase.java
new file mode 100644
index 00000000000..70b3b009ee6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/IndexSettingsTestCase.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Rank settings
+ *
+ * @author bratseth
+ */
+public class IndexSettingsTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testStemmingSettings() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/indexsettings.sd");
+
+ SDField usingDefault=(SDField) search.getDocument().getField("usingdefault");
+ assertEquals(Stemming.SHORTEST,usingDefault.getStemming(search));
+
+ SDField notStemmed=(SDField) search.getDocument().getField("notstemmed");
+ assertEquals(Stemming.NONE,notStemmed.getStemming(search));
+
+ SDField allStemmed=(SDField) search.getDocument().getField("allstemmed");
+ assertEquals(Stemming.SHORTEST,allStemmed.getStemming(search));
+
+ SDField multiStemmed=(SDField) search.getDocument().getField("multiplestems");
+ assertEquals(Stemming.MULTIPLE, multiStemmed.getStemming(search));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/IndexingParsingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/IndexingParsingTestCase.java
new file mode 100644
index 00000000000..35f823ecd30
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/IndexingParsingTestCase.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Tests that indexing statements are parsed correctly.
+ *
+ * @author <a href="mailto:frodelu@yahoo-inc.com">Frode Lundgren</a>
+ */
+public class IndexingParsingTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void requireThatIndexingExpressionsCanBeParsed() throws Exception {
+ assertNotNull(SearchBuilder.buildFromFile("src/test/examples/indexing.sd"));
+ }
+
+ @Test
+ public void requireThatParseExceptionPositionIsCorrect() throws Exception {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/indexing_invalid_expression.sd");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("at line 5, column 57.")) {
+ throw e;
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/MultipleSummariesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/MultipleSummariesTestCase.java
new file mode 100644
index 00000000000..250db2609e2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/MultipleSummariesTestCase.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * tests importing of document containing array type fields
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class MultipleSummariesTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testArrayImporting() throws IOException, ParseException {
+ SearchBuilder.buildFromFile("src/test/examples/multiplesummaries.sd");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/NameFieldCheckTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/NameFieldCheckTestCase.java
new file mode 100644
index 00000000000..f8fe979c866
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/NameFieldCheckTestCase.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+/**
+ * Tests that "name" is not allowed as name for a field.
+ *
+ * And that duplicate names are not allowed.
+ *
+ * @author <a href="mailto:larschr@yahoo-inc.com">Lars Christian Jensen</a>
+ */
+public class NameFieldCheckTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testNameField() throws IOException, ParseException {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/name-check.sd");
+ fail("Should throw exception.");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testDuplicateNamesInSearchDifferentType() {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/duplicatenamesinsearchdifferenttype.sd");
+ fail("Should throw exception.");
+ } catch (Exception e) {
+ e.printStackTrace();
+ assertTrue(e.getMessage().matches(".*Duplicate.*different type.*"));
+ }
+ }
+
+ @Test
+ public void testDuplicateNamesInDoc() {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/duplicatenamesindoc.sd");
+ fail("Should throw exception.");
+ } catch (Exception e) {
+ e.printStackTrace();
+ assertTrue(e.getMessage().matches(".*Duplicate.*"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/OutsideTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/OutsideTestCase.java
new file mode 100644
index 00000000000..c71d089e4d4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/OutsideTestCase.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+/**
+ * Tests settings outside the document
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class OutsideTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testOutsideIndex() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/outsidedoc.sd");
+
+ Index defaultIndex=search.getIndex("default");
+ assertTrue(defaultIndex.isPrefix());
+ assertEquals("default.default",defaultIndex.aliasIterator().next());
+ }
+ @Test
+ public void testOutsideSummary() throws IOException, ParseException {
+ SearchBuilder.buildFromFile("src/test/examples/outsidesummary.sd");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/PredicateDataTypeTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/PredicateDataTypeTestCase.java
new file mode 100644
index 00000000000..09b6a8e7b8b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/PredicateDataTypeTestCase.java
@@ -0,0 +1,196 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import static org.junit.Assert.*;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+
+/**
+ * @author <a href="mailto:lesters@yahoo-inc.com">Lester Solbakken</a>
+ * @since 5.2
+ */
+
+public class PredicateDataTypeTestCase {
+
+ private String searchSd(String field) {
+ return "search p {\n document p {\n" + field + "}\n}\n";
+ }
+
+ private String predicateFieldSd(String index) {
+ return "field pf type predicate {\n" + index + "}\n";
+ }
+
+ private String arrayPredicateFieldSd(String index) {
+ return "field apf type array<predicate> {\n" + index + "}\n";
+ }
+
+ private String stringFieldSd(String index) {
+ return "field sf type string {\n" + index + "}\n";
+ }
+
+ private String attributeFieldSd(String terms) {
+ return "indexing: attribute\n index {\n" + terms + "}\n";
+ }
+
+ private String arityParameter(int arity) {
+ return "arity: " + arity + "\n";
+ }
+
+ private String lowerBoundParameter(long bound) {
+ return "lower-bound: " + bound + "\n";
+ }
+
+ private String upperBoundParameter(long bound) {
+ return "upper-bound: " + bound + "\n";
+ }
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void requireThatBuilderSetsIndexParametersCorrectly() throws ParseException {
+ int arity = 2;
+ long lowerBound = -100;
+ long upperBound = 100;
+ String sd = searchSd(
+ predicateFieldSd(
+ attributeFieldSd(
+ arityParameter(arity) +
+ lowerBoundParameter(lowerBound) +
+ upperBoundParameter(upperBound))));
+
+ SearchBuilder sb = SearchBuilder.createFromString(sd);
+ for (SDField field : sb.getSearch().allFieldsList()) {
+ if (field.getDataType() == DataType.PREDICATE) {
+ for (Index index : field.getIndices().values()) {
+ assertEquals(true, index.getBooleanIndexDefiniton().hasArity());
+ assertEquals(arity, index.getBooleanIndexDefiniton().getArity());
+ assertEquals(true, index.getBooleanIndexDefiniton().hasLowerBound());
+ assertEquals(lowerBound, index.getBooleanIndexDefiniton().getLowerBound());
+ assertEquals(true, index.getBooleanIndexDefiniton().hasUpperBound());
+ assertEquals(upperBound, index.getBooleanIndexDefiniton().getUpperBound());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void requireThatBuilderHandlesLongValues() throws ParseException {
+ int arity = 2;
+ long lowerBound = -100000000000000000L;
+ long upperBound = 1000000000000000000L;
+ String sd = searchSd(
+ predicateFieldSd(
+ attributeFieldSd(
+ arityParameter(arity) +
+ "lower-bound: -100000000000000000L\n" + // +'L'
+ upperBoundParameter(upperBound))));
+
+ SearchBuilder sb = SearchBuilder.createFromString(sd);
+ for (SDField field : sb.getSearch().allFieldsList()) {
+ if (field.getDataType() == DataType.PREDICATE) {
+ for (Index index : field.getIndices().values()) {
+ assertEquals(arity, index.getBooleanIndexDefiniton().getArity());
+ assertEquals(lowerBound, index.getBooleanIndexDefiniton().getLowerBound());
+ assertEquals(upperBound, index.getBooleanIndexDefiniton().getUpperBound());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void requireThatBuilderHandlesMissingParameters() throws ParseException {
+ String sd = searchSd(
+ predicateFieldSd(
+ attributeFieldSd(
+ arityParameter(2))));
+ SearchBuilder sb = SearchBuilder.createFromString(sd);
+ for (SDField field : sb.getSearch().allFieldsList()) {
+ if (field.getDataType() == DataType.PREDICATE) {
+ for (Index index : field.getIndices().values()) {
+ assertEquals(true, index.getBooleanIndexDefiniton().hasArity());
+ assertEquals(false, index.getBooleanIndexDefiniton().hasLowerBound());
+ assertEquals(false, index.getBooleanIndexDefiniton().hasUpperBound());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfNoArityValue() throws ParseException {
+ String sd = searchSd(predicateFieldSd(attributeFieldSd("")));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Missing arity value in predicate field.");
+ SearchBuilder.createFromString(sd);
+ assertTrue(false);
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfBothIndexAndAttribute() throws ParseException {
+ String sd = searchSd(predicateFieldSd("indexing: summary | index | attribute\nindex { arity: 2 }"));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For search 'p', field 'pf': Use 'attribute' instead of 'index'. This will require a refeed if you have upgraded.");
+ SearchBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfIndex() throws ParseException {
+ String sd = searchSd(predicateFieldSd("indexing: summary | index \nindex { arity: 2 }"));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For search 'p', field 'pf': Use 'attribute' instead of 'index'. This will require a refeed if you have upgraded.");
+ SearchBuilder.createFromString(sd);
+ }
+
+
+ @Test
+ public void requireThatBuilderFailsIfIllegalArityValue() throws ParseException {
+ String sd = searchSd(predicateFieldSd(attributeFieldSd(arityParameter(0))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Invalid arity value in predicate field, must be greater than 1.");
+ SearchBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfArityParameterExistButNotPredicateField() throws ParseException {
+ String sd = searchSd(stringFieldSd(attributeFieldSd(arityParameter(2))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Arity parameter is used only for predicate type fields.");
+ SearchBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatBuilderFailsIfBoundParametersExistButNotPredicateField() throws ParseException {
+ String sd = searchSd(
+ stringFieldSd(
+ attributeFieldSd(
+ lowerBoundParameter(100) + upperBoundParameter(1000))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Parameters lower-bound and upper-bound are used only for predicate type fields.");
+ SearchBuilder.createFromString(sd);
+ }
+
+ @Test
+ public void requireThatArrayOfPredicateFails() throws ParseException {
+ String sd = searchSd(
+ arrayPredicateFieldSd(
+ attributeFieldSd(
+ arityParameter(1))));
+
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("Collections of predicates are not allowed.");
+ SearchBuilder.createFromString(sd);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/RankProfileRegistryTest.java b/config-model/src/test/java/com/yahoo/searchdefinition/RankProfileRegistryTest.java
new file mode 100644
index 00000000000..570c835c617
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/RankProfileRegistryTest.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.model.test.TestRoot;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.20
+ */
+public class RankProfileRegistryTest {
+ private static final String TESTDIR = "src/test/cfg/search/data/v2/inherited_rankprofiles";
+
+ @Test
+ public void testRankProfileInheritance() {
+ TestRoot root = new TestDriver().buildModel(FilesApplicationPackage.fromFile(new File(TESTDIR)));
+ RankProfilesConfig left = root.getConfig(RankProfilesConfig.class, "inherit/search/cluster.inherit/left");
+ RankProfilesConfig right = root.getConfig(RankProfilesConfig.class, "inherit/search/cluster.inherit/right");
+ System.out.println(left);
+ assertThat(left.rankprofile().size(), is(3));
+ System.out.println("\n\n");
+ System.out.println(right);
+ assertThat(right.rankprofile().size(), is(2));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRankProfileDuplicateNameIsIllegal() {
+ Search search = new Search("foo", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+ final RankProfile barRankProfile = new RankProfile("bar", search, rankProfileRegistry);
+ rankProfileRegistry.addRankProfile(barRankProfile);
+ rankProfileRegistry.addRankProfile(barRankProfile);
+ }
+
+ @Test
+ public void testRankProfileDuplicateNameLegalForOverridableRankProfiles() {
+ Search search = new Search("foo", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+
+ for (String rankProfileName : RankProfileRegistry.overridableRankProfileNames) {
+ assertNull(rankProfileRegistry.getRankProfile(search, rankProfileName).getMacros().get("foo"));
+ final RankProfile rankProfileWithAddedMacro = new RankProfile(rankProfileName, search, rankProfileRegistry);
+ rankProfileWithAddedMacro.addMacro("foo", true);
+ rankProfileRegistry.addRankProfile(rankProfileWithAddedMacro);
+ assertNotNull(rankProfileRegistry.getRankProfile(search, rankProfileName).getMacros().get("foo"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/RankProfileTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/RankProfileTestCase.java
new file mode 100644
index 00000000000..79ee2df03a0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/RankProfileTestCase.java
@@ -0,0 +1,165 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+import com.yahoo.searchdefinition.derived.AttributeFields;
+import com.yahoo.searchdefinition.derived.RawRankProfile;
+import com.yahoo.searchdefinition.document.RankType;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests rank profiles
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class RankProfileTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testRankProfileInheritance() {
+ Search search = new Search("test", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+ SDDocumentType document = new SDDocumentType("test");
+ SDField a = document.addField("a", DataType.STRING);
+ a.setRankType(RankType.IDENTITY);
+ document.addField("b", DataType.STRING);
+ search.addDocument(document);
+ RankProfile child = new RankProfile("child", search, rankProfileRegistry);
+ child.setInherited("default");
+ rankProfileRegistry.addRankProfile(child);
+
+ Iterator<RankProfile.RankSetting> i = child.rankSettingIterator();
+
+ RankProfile.RankSetting setting = i.next();
+ assertEquals(RankType.IDENTITY, setting.getValue());
+ assertEquals("a", setting.getFieldName());
+ assertEquals(RankProfile.RankSetting.Type.RANKTYPE, setting.getType());
+
+ setting = i.next();
+ assertEquals(RankType.DEFAULT, setting.getValue());
+ }
+
+ @Test
+ public void testTermwiseLimitAndSomeMore() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " termwise-limit:0.78\n" +
+ " num-threads-per-search:8\n" +
+ " num-search-partitions:1200\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search search = builder.getSearch();
+ RankProfile rankProfile = rankProfileRegistry.getRankProfile(search, "parent");
+ assertEquals(0.78, rankProfile.getTermwiseLimit(), 0.000001);
+ assertEquals(8, rankProfile.getNumThreadsPerSearch());
+ assertEquals(1200, rankProfile.getNumSearchPartitions());
+ AttributeFields attributeFields = new AttributeFields(search);
+ RawRankProfile rawRankProfile = new RawRankProfile(rankProfile, attributeFields);
+ assertTrue(rawRankProfile.configProperties().containsKey("vespa.matching.termwise_limit"));
+ assertEquals("0.78", rawRankProfile.configProperties().get("vespa.matching.termwise_limit"));
+ assertTrue(rawRankProfile.configProperties().containsKey("vespa.matching.numthreadspersearch"));
+ assertEquals("8", rawRankProfile.configProperties().get("vespa.matching.numthreadspersearch"));
+ assertTrue(rawRankProfile.configProperties().containsKey("vespa.matching.numsearchpartitions"));
+ assertEquals("1200", rawRankProfile.configProperties().get("vespa.matching.numsearchpartitions"));
+ }
+
+ @Test
+ public void requireThatConfigIsDerivedForAttributeTypeSettings() throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(registry);
+ builder.importString("search test {\n" +
+ " document test { \n" +
+ " field a type tensor { indexing: attribute \n attribute: tensor(x[10]) }\n" +
+ " field b type tensor { indexing: attribute \n attribute: tensor(y{}) }\n" +
+ " field c type tensor { indexing: attribute }\n" +
+ " }\n" +
+ " rank-profile p1 {}\n" +
+ " rank-profile p2 {}\n" +
+ "}");
+ builder.build();
+ Search search = builder.getSearch();
+
+ assertEquals(4, registry.allRankProfiles().size());
+ assertAttributeTypeSettings(registry.getRankProfile(search, "default"), search);
+ assertAttributeTypeSettings(registry.getRankProfile(search, "unranked"), search);
+ assertAttributeTypeSettings(registry.getRankProfile(search, "p1"), search);
+ assertAttributeTypeSettings(registry.getRankProfile(search, "p2"), search);
+ }
+
+ private static void assertAttributeTypeSettings(RankProfile profile, Search search) {
+ RawRankProfile rawProfile = new RawRankProfile(profile, new AttributeFields(search));
+ assertEquals("tensor(x[10])", rawProfile.configProperties().get("vespa.type.attribute.a"));
+ assertEquals("tensor(y{})", rawProfile.configProperties().get("vespa.type.attribute.b"));
+ assertFalse(rawProfile.configProperties().containsKey("vespa.type.attribute.c"));
+ }
+
+ @Test
+ public void requireThatConfigIsDerivedForQueryFeatureTypeSettings() throws ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(registry);
+ builder.importString("search test {\n" +
+ " document test { } \n" +
+ " rank-profile p1 {}\n" +
+ " rank-profile p2 {}\n" +
+ "}");
+ builder.build(new BaseDeployLogger(), setupQueryProfileTypes());
+ Search search = builder.getSearch();
+
+ assertEquals(4, registry.allRankProfiles().size());
+ assertQueryFeatureTypeSettings(registry.getRankProfile(search, "default"), search);
+ assertQueryFeatureTypeSettings(registry.getRankProfile(search, "unranked"), search);
+ assertQueryFeatureTypeSettings(registry.getRankProfile(search, "p1"), search);
+ assertQueryFeatureTypeSettings(registry.getRankProfile(search, "p2"), search);
+ }
+
+ private static QueryProfiles setupQueryProfileTypes() {
+ QueryProfileRegistry registry = new QueryProfileRegistry();
+ QueryProfileTypeRegistry typeRegistry = registry.getTypeRegistry();
+ QueryProfileType type = new QueryProfileType(new ComponentId("testtype"));
+ type.addField(new FieldDescription("ranking.features.query(tensor1)",
+ FieldType.fromString("tensor(x[10])", typeRegistry)), typeRegistry);
+ type.addField(new FieldDescription("ranking.features.query(tensor2)",
+ FieldType.fromString("tensor(y{})", typeRegistry)), typeRegistry);
+ type.addField(new FieldDescription("ranking.features.invalid(tensor3)",
+ FieldType.fromString("tensor(x{})", typeRegistry)), typeRegistry);
+ type.addField(new FieldDescription("ranking.features.query(numeric)",
+ FieldType.fromString("integer", typeRegistry)), typeRegistry);
+ typeRegistry.register(type);
+ return new QueryProfiles(registry);
+ }
+
+ private static void assertQueryFeatureTypeSettings(RankProfile profile, Search search) {
+ RawRankProfile rawProfile = new RawRankProfile(profile, new AttributeFields(search));
+ assertEquals("tensor(x[10])", rawProfile.configProperties().get("vespa.type.query.tensor1"));
+ assertEquals("tensor(y{})", rawProfile.configProperties().get("vespa.type.query.tensor2"));
+ assertFalse(rawProfile.configProperties().containsKey("vespa.type.query.tensor3"));
+ assertFalse(rawProfile.configProperties().containsKey("vespa.type.query.numeric"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/RankPropertiesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/RankPropertiesTestCase.java
new file mode 100644
index 00000000000..05548e62c2b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/RankPropertiesTestCase.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.derived.AttributeFields;
+import com.yahoo.searchdefinition.derived.RawRankProfile;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RankPropertiesTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testRankPropertyInheritance() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " first-phase {\n" +
+ " expression: a\n" +
+ " }\n" +
+ " rank-properties {\n" +
+ " query(a): 1500 \n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child inherits parent {\n" +
+ " first-phase {\n" +
+ " expression: a\n" +
+ " }\n" +
+ " rank-properties {\n" +
+ " query(a): 2000 \n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search search = builder.getSearch();
+ AttributeFields attributeFields = new AttributeFields(search);
+
+ {
+ // Check declared model
+ RankProfile parent = rankProfileRegistry.getRankProfile(search, "parent");
+ assertEquals("query(a) = 1500", parent.getRankProperties().get(0).toString());
+
+ // Check derived model
+ RawRankProfile rawParent = new RawRankProfile(parent, attributeFields);
+ List<Map.Entry<String, Object>> parentProperties = new ArrayList<>(rawParent.configProperties().entrySet());
+ assertEquals("query(a).part0=1500", parentProperties.get(0).toString());
+ }
+
+ {
+ // Check declared model
+ RankProfile parent = rankProfileRegistry.getRankProfile(search, "child");
+ assertEquals("query(a) = 2000", parent.getRankProperties().get(0).toString());
+
+ // Check derived model
+ RawRankProfile rawChild = new RawRankProfile(rankProfileRegistry.getRankProfile(search, "child"), attributeFields);
+ List<Map.Entry<String, Object>> childProperties = new ArrayList<>(rawChild.configProperties().entrySet());
+ assertEquals("query(a).part0=2000", childProperties.get(0).toString());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionConstantsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionConstantsTestCase.java
new file mode 100644
index 00000000000..fa20e2a5eaf
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionConstantsTestCase.java
@@ -0,0 +1,218 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.yolean.Exceptions;
+import com.yahoo.searchdefinition.derived.AttributeFields;
+import com.yahoo.searchdefinition.derived.RawRankProfile;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RankingExpressionConstantsTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testConstants() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " constants {\n" +
+ " p1: 7 \n" +
+ " p2: 0 \n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: p2 * (1.3 + p1 )\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child1 inherits parent {\n" +
+ " first-phase {\n" +
+ " expression: a + b + c \n" +
+ " }\n" +
+ " second-phase {\n" +
+ " expression: a + p1 + c \n" +
+ " }\n" +
+ " constants {\n" +
+ " a: 1.0 \n" +
+ " b: 2 \n" +
+ " c: 3.5 \n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child2 inherits parent {\n" +
+ " constants {\n" +
+ " p2: 2.0 \n" +
+ " }\n" +
+ " macro foo() {\n" +
+ " expression: p2*p1\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+ RankProfile parent = rankProfileRegistry.getRankProfile(s, "parent").compile();
+ assertEquals("0.0", parent.getFirstPhaseRanking().getRoot().toString());
+
+ RankProfile child1 = rankProfileRegistry.getRankProfile(s, "child1").compile();
+ assertEquals("6.5", child1.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("11.5", child1.getSecondPhaseRanking().getRoot().toString());
+
+ RankProfile child2 = rankProfileRegistry.getRankProfile(s, "child2").compile();
+ assertEquals("16.6", child2.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("foo: 14.0", child2.getMacros().get("foo").getRankingExpression().toString());
+ List<Map.Entry<String, Object>> rankProperties = new ArrayList<>(new RawRankProfile(child2, new AttributeFields(s)).configProperties().entrySet());
+ assertEquals("rankingExpression(foo).rankingScript.part0=14.0", rankProperties.get(0).toString());
+ assertEquals("rankingExpression(firstphase).rankingScript=16.6", rankProperties.get(2).toString());
+ }
+
+ @Test
+ public void testNameCollision() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " constants {\n" +
+ " c: 7 \n" +
+ " }\n" +
+ " macro c() {\n" +
+ " expression: p2*p1\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+ try {
+ rankProfileRegistry.getRankProfile(s, "test").compile();
+ fail("Should have caused an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("Rank profile 'test' is invalid: Cannot have both a constant and macro named 'c'",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+ @Test
+ public void testNegativeLiteralArgument() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " macro POP_SLOW_SCORE() {\n" +
+ " expression: safeLog(popShareSlowDecaySignal, -9.21034037)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+ RankProfile profile = rankProfileRegistry.getRankProfile(s, "test");
+ profile.parseExpressions(); // TODO: Do differently
+ assertEquals("safeLog(popShareSlowDecaySignal,-9.21034037)", profile.getMacros().get("POP_SLOW_SCORE").getRankingExpression().getRoot().toString());
+ }
+
+ @Test
+ public void testNegativeConstantArgument() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " constants {\n" +
+ " myValue: -9.21034037\n" +
+ " }\n" +
+ " macro POP_SLOW_SCORE() {\n" +
+ " expression: safeLog(popShareSlowDecaySignal, myValue)\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+ RankProfile profile = rankProfileRegistry.getRankProfile(s, "test");
+ profile.parseExpressions(); // TODO: Do differently
+ assertEquals("safeLog(popShareSlowDecaySignal,myValue)", profile.getMacros().get("POP_SLOW_SCORE").getRankingExpression().getRoot().toString());
+ assertEquals("safeLog(popShareSlowDecaySignal,-9.21034037)", profile.compile().getMacros().get("POP_SLOW_SCORE").getRankingExpression().getRoot().toString());
+ }
+
+ @Test
+ public void testConstantDivisorInMacro() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " macro rank_default(){\n" +
+ " expression: k1 + (k2 + k3) / 100000000.0\n\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+ RankProfile profile = rankProfileRegistry.getRankProfile(s, "test");
+ assertEquals("k1 + (k2 + k3) / 100000000.0", profile.compile().getMacros().get("rank_default").getRankingExpression().getRoot().toString());
+ }
+
+ @Test
+ public void test3() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " }\n" +
+ " \n" +
+ " rank-profile test {\n" +
+ " macro rank_default(){\n" +
+ " expression: 0.5+50*(attribute(rating_yelp)-3)\n\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+ RankProfile profile = rankProfileRegistry.getRankProfile(s, "test");
+ assertEquals("0.5 + 50 * (attribute(rating_yelp) - 3)", profile.compile().getMacros().get("rank_default").getRankingExpression().getRoot().toString());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionInliningTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionInliningTestCase.java
new file mode 100644
index 00000000000..f15e5c06012
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionInliningTestCase.java
@@ -0,0 +1,118 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.derived.AttributeFields;
+import com.yahoo.searchdefinition.derived.RawRankProfile;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RankingExpressionInliningTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testConstants() throws ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ builder.importString(
+ "search test {\n" +
+ " document test { \n" +
+ " field a type string { \n" +
+ " indexing: index \n" +
+ " }\n" +
+ " }\n" +
+ " \n" +
+ " rank-profile parent {\n" +
+ " constants {\n" +
+ " p1: 7 \n" +
+ " p2: 0 \n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: p1 + foo\n" +
+ " }\n" +
+ " second-phase {\n" +
+ " expression: p2 * foo\n" +
+ " }\n" +
+ " macro inline foo() {\n" +
+ " expression: 3 + p1 + p2\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile child inherits parent {\n" +
+ " first-phase {\n" +
+ " expression: p1 + foo + baz + bar + arg(4.0)\n" +
+ " }\n" +
+ " constants {\n" +
+ " p2: 2.0 \n" +
+ " }\n" +
+ " macro bar() {\n" +
+ " expression: p2*p1\n" +
+ " }\n" +
+ " macro inline baz() {\n" +
+ " expression: p2+p1+boz\n" +
+ " }\n" +
+ " macro inline boz() {\n" +
+ " expression: 3.0\n" +
+ " }\n" +
+ " macro inline arg(a1) {\n" +
+ " expression: a1*2\n" +
+ " }\n" +
+ " }\n" +
+ "\n" +
+ "}\n");
+ builder.build();
+ Search s = builder.getSearch();
+
+ RankProfile parent = rankProfileRegistry.getRankProfile(s, "parent").compile();
+ assertEquals("17.0", parent.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("0.0", parent.getSecondPhaseRanking().getRoot().toString());
+ List<Map.Entry<String, Object>> parentRankProperties = new ArrayList<>(new RawRankProfile(parent, new AttributeFields(s)).configProperties().entrySet());
+ assertEquals("rankingExpression(foo).rankingScript.part0=10.0", parentRankProperties.get(0).toString());
+ assertEquals("rankingExpression(firstphase).rankingScript=17.0", parentRankProperties.get(2).toString());
+ assertEquals("rankingExpression(secondphase).rankingScript=0.0", parentRankProperties.get(4).toString());
+
+ RankProfile child = rankProfileRegistry.getRankProfile(s, "child").compile();
+ assertEquals("31.0 + bar + arg(4.0)", child.getFirstPhaseRanking().getRoot().toString());
+ assertEquals("24.0", child.getSecondPhaseRanking().getRoot().toString());
+ List<Map.Entry<String, Object>> childRankProperties = new ArrayList<>(new RawRankProfile(child, new AttributeFields(s)).configProperties().entrySet());
+ for (Object o : childRankProperties) System.out.println(o);
+ assertEquals("rankingExpression(foo).rankingScript.part0=12.0", childRankProperties.get(0).toString());
+ assertEquals("rankingExpression(bar).rankingScript.part1=14.0", childRankProperties.get(1).toString());
+ assertEquals("rankingExpression(boz).rankingScript.part2=3.0", childRankProperties.get(2).toString());
+ assertEquals("rankingExpression(baz).rankingScript.part3=9.0 + rankingExpression(boz)", childRankProperties.get(3).toString());
+ assertEquals("rankingExpression(arg).rankingScript.part4=a1 * 2", childRankProperties.get(4).toString());
+ assertEquals("rankingExpression(firstphase).rankingScript=31.0 + rankingExpression(bar) + rankingExpression(arg@)", censorBindingHash(childRankProperties.get(7).toString()));
+ assertEquals("rankingExpression(secondphase).rankingScript=24.0", childRankProperties.get(9).toString());
+ }
+
+ /**
+ * Expression evaluation has no stack so macro arguments are bound at config time creating a separate version of
+ * each macro for each binding, using hashes to name the bound variants of the macro.
+ * This method censors those hashes for string comparison.
+ */
+ private String censorBindingHash(String s) {
+ StringBuilder b = new StringBuilder();
+ boolean areInHash = false;
+ for (int i = 0; i < s.length() ; i++) {
+ char current = s.charAt(i);
+
+ if ( ! Character.isLetterOrDigit(current)) // end of hash
+ areInHash = false;
+
+ if ( ! areInHash)
+ b.append(current);
+
+ if (current == '@') // start of hash
+ areInHash = true;
+ }
+ return b.toString();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionValidationTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionValidationTestCase.java
new file mode 100644
index 00000000000..0c7a75d5694
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/RankingExpressionValidationTestCase.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RankingExpressionValidationTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testInvalidExpressionProducesException() throws ParseException {
+ assertFailsExpression("&/%(/%&");
+ assertFailsExpression("if(a==b,b)");
+ }
+
+ private void assertFailsExpression(String expression) throws ParseException {
+ try {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ Search search = importWithExpression(expression, registry);
+ new DerivedConfiguration(search, registry); // rank profile parsing happens during deriving
+ fail("No exception on incorrect ranking expression " + expression);
+ } catch (IllegalArgumentException e) {
+ // Success
+ // TODO: Where's the "com.yahoo.searchdefinition.parser.ParseException:" nonsense coming from?
+ assertTrue("Got unexpected error message: " + e.getCause().getMessage(),
+ e.getCause().getMessage().startsWith("com.yahoo.searchdefinition.parser.ParseException: Could not parse ranking expression '" + expression + "'"));
+ }
+ }
+
+ private Search importWithExpression(String expression, RankProfileRegistry registry) throws ParseException {
+ SearchBuilder builder = new SearchBuilder(registry);
+ builder.importString("search test {" +
+ " document test { " +
+ " field a type string { " +
+ " indexing: index " +
+ " }" +
+ " }" +
+ " rank-profile default {" +
+ " first-phase {" +
+ " expression: " + expression +
+ " }" +
+ " }" +
+ "}");
+ builder.build();
+ return builder.getSearch();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/ReservedWordsAsFieldNamesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/ReservedWordsAsFieldNamesTestCase.java
new file mode 100644
index 00000000000..4183ffe64e3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/ReservedWordsAsFieldNamesTestCase.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ReservedWordsAsFieldNamesTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testIt() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/reserved_words_as_field_names.sd");
+ assertNotNull(search.getDocument().getField("inline"));
+ assertNotNull(search.getDocument().getField("constants"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/SDDocumentTypeOrdererTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/SDDocumentTypeOrdererTestCase.java
new file mode 100755
index 00000000000..1e5db53c3e1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/SDDocumentTypeOrdererTestCase.java
@@ -0,0 +1,76 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.document.DataTypeName;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.TemporarySDDocumentType;
+import com.yahoo.searchdefinition.document.TemporarySDField;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static junit.framework.TestCase.assertEquals;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class SDDocumentTypeOrdererTestCase {
+ @Test
+ public void testOrder() {
+ List<SDDocumentType> types = new ArrayList<>();
+
+ SDDocumentType a = new SDDocumentType("a");
+ SDDocumentType b = new SDDocumentType("b");
+ SDDocumentType c = new SDDocumentType("c");
+ SDDocumentType d = new SDDocumentType("d");
+ SDDocumentType e = new SDDocumentType("e");
+ SDDocumentType f = new SDDocumentType("f");
+ SDDocumentType g = new SDDocumentType("g");
+ b.inherit(new TemporarySDDocumentType(new DataTypeName("a")));
+ c.inherit(new TemporarySDDocumentType(new DataTypeName("b")));
+ d.inherit(new TemporarySDDocumentType(new DataTypeName("e")));
+ g.inherit(new TemporarySDDocumentType(new DataTypeName("e")));
+ g.inherit(new TemporarySDDocumentType(new DataTypeName("c")));
+
+ SDField aFieldTypeB = new TemporarySDField("atypeb", DataType.STRING);
+ a.addField(aFieldTypeB);
+
+ SDField bFieldTypeC = new TemporarySDField("btypec", DataType.STRING);
+ b.addField(bFieldTypeC);
+
+ SDField cFieldTypeG = new TemporarySDField("ctypeg", DataType.STRING);
+ c.addField(cFieldTypeG);
+
+ SDField gFieldTypeF = new TemporarySDField("gtypef", DataType.STRING);
+ g.addField(gFieldTypeF);
+
+ SDField fFieldTypeC = new TemporarySDField("ftypec", DataType.STRING);
+ f.addField(fFieldTypeC);
+
+ SDField dFieldTypeE = new TemporarySDField("dtypee", DataType.STRING);
+ d.addField(dFieldTypeE);
+
+ types.add(a);
+ types.add(b);
+ types.add(c);
+ types.add(d);
+ types.add(e);
+ types.add(f);
+ types.add(g);
+
+ SDDocumentTypeOrderer app = new SDDocumentTypeOrderer(types, new BaseDeployLogger());
+ app.process();
+ assertEquals(7, app.processingOrder.size());
+ assertEquals(a, app.processingOrder.get(0));
+ assertEquals(b, app.processingOrder.get(1));
+ assertEquals(c, app.processingOrder.get(2));
+ assertEquals(e, app.processingOrder.get(3));
+ assertEquals(d, app.processingOrder.get(4));
+ assertEquals(f, app.processingOrder.get(5));
+ assertEquals(g, app.processingOrder.get(6));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionTestCase.java
new file mode 100644
index 00000000000..774f75e059c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionTestCase.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.io.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+
+import static helpers.CompareConfigTestHelper.assertSerializedConfigEquals;
+import static helpers.CompareConfigTestHelper.assertSerializedConfigFileEquals;
+import static org.junit.Assert.assertEquals;
+
+public abstract class SearchDefinitionTestCase {
+
+ public static void assertConfigFile(String filename, String cfg) throws IOException {
+ assertSerializedConfigFileEquals(filename, cfg);
+ }
+
+ public static void assertConfigFiles(String expectedFile, String cfgFile) throws IOException {
+ try {
+ assertSerializedConfigEquals(readAndCensorIndexes(expectedFile), readAndCensorIndexes(cfgFile));
+ } catch (AssertionError e) {
+ throw new AssertionError(e.getMessage() + " [not equal files: >>>"+expectedFile+"<<< and >>>"+cfgFile+"<<< in assertConfigFiles]", e);
+ }
+ }
+
+ /**
+ * This is to avoid having to keep those pesky array index numbers in the config format up to date
+ * as new entries are added and removed.
+ */
+ public static String readAndCensorIndexes(String file) throws IOException {
+ StringBuilder b = new StringBuilder();
+ try (BufferedReader r = IOUtils.createReader(file)) {
+ int character;
+ boolean lastWasNewline = false;
+ boolean inBrackets = false;
+ while (-1 != (character = r.read())) {
+ // skip empty lines
+ if (character == '\n') {
+ if (lastWasNewline) continue;
+ lastWasNewline = true;
+ }
+ else {
+ lastWasNewline = false;
+ }
+
+ // skip quoted strings
+ if (character == '"') {
+ b.appendCodePoint(character);
+ while (-1 != (character = r.read()) && character != '"') {
+ b.appendCodePoint(character);
+ }
+ }
+
+ // skip bracket content
+ if (character == ']')
+ inBrackets = false;
+ if (! inBrackets)
+ b.appendCodePoint(character);
+ if (character == '[')
+ inBrackets = true;
+ }
+ }
+ return b.toString();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionsParsingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionsParsingTestCase.java
new file mode 100644
index 00000000000..ec4ef766745
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/SearchDefinitionsParsingTestCase.java
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import java.io.IOException;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests that search definitions are parsed correctly and that correct line number is reported in
+ * error message.
+ *
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class SearchDefinitionsParsingTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void requireThatIndexingExpressionsCanBeParsed() throws Exception {
+ assertNotNull(SearchBuilder.buildFromFile("src/test/examples/simple.sd"));
+ }
+
+ @Test
+ public void requireThatParseExceptionPositionIsCorrect() throws Exception {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/invalid_sd_construct.sd");
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ if (!e.getMessage().contains("at line 5, column 36.")) {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void requireThatParserHandlesLexicalError() throws Exception {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/invalid_sd_lexical_error.sd");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("at line 7, column 27.")) {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void requireErrorWhenJunkAfterSearchBlock() throws IOException, ParseException {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/invalid_sd_junk_at_end.sd");
+ fail("Illegal junk at end of SD passed");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("at line 10, column 1")) {
+ throw e;
+ }
+ }
+ }
+
+ @Test
+ public void requireErrorWhenMissingClosingSearchBracket() throws IOException, ParseException {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/invalid_sd_no_closing_bracket.sd");
+ fail("SD without closing bracket passed");
+ } catch (ParseException e) {
+ if (!e.getMessage().contains("Encountered \"<EOF>\" at line 8, column 1")) {
+ throw e;
+ }
+ }
+ }
+
+ private static class WarningCatcher extends Handler {
+ volatile boolean gotYqlWarning = false;
+
+ @Override
+ public void publish(LogRecord record) {
+ if (record.getLevel() == Level.WARNING && record.getMessage().indexOf("YQL") >= 0) {
+ gotYqlWarning = true;
+ }
+ }
+
+ @Override
+ public void flush() {
+ // intentionally left blank
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ // intentionally left blank
+ }
+ }
+
+
+ @Test
+ public void requireYqlCompatibilityIsTested() throws Exception {
+ Logger log = Logger.getLogger("DeployLogger");
+ WarningCatcher w = new WarningCatcher();
+ log.addHandler(w);
+ assertNotNull(SearchBuilder.buildFromFile("src/test/examples/simple-with-weird-name.sd"));
+ log.removeHandler(w);
+ assertTrue(w.gotYqlWarning);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/SearchImporterTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/SearchImporterTestCase.java
new file mode 100644
index 00000000000..bb9680665c8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/SearchImporterTestCase.java
@@ -0,0 +1,180 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.document.Document;
+import com.yahoo.searchdefinition.document.*;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.searchdefinition.processing.MakeAliases;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests importing of search definitions
+ *
+ * @author bratseth
+ */
+public class SearchImporterTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testSimpleImporting() throws IOException, ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ SearchBuilder sb = new UnprocessingSearchBuilder(rankProfileRegistry);
+ sb.importFile("src/test/examples/simple.sd");
+ sb.build();
+ Search search = sb.getSearch();
+ assertEquals("simple",search.getName());
+ assertTrue(search.hasDocument());
+
+ SDDocumentType document=search.getDocument();
+ assertEquals("simple",document.getName());
+ assertEquals(12,document.getFieldCount());
+
+ SDField field;
+ Attribute attribute;
+
+ new MakeAliases(search, new BaseDeployLogger(), rankProfileRegistry, new QueryProfiles()).process();
+
+ // First field
+ field=(SDField) document.getField("title");
+ assertEquals(DataType.STRING,field.getDataType());
+ assertEquals("{ summary | index; }",
+ field.getIndexingScript().toString());
+ assertTrue(!search.getIndex("default").isPrefix());
+ assertTrue(search.getIndex("title").isPrefix());
+ Iterator<String> titleAliases=search.getIndex("title").aliasIterator();
+ assertEquals("aliaz",titleAliases.next());
+ assertEquals("analias.totitle",titleAliases.next());
+ assertEquals("analias.todefault",
+ search.getIndex("default").aliasIterator().next());
+ assertEquals(RankType.IDENTITY, field.getRankType());
+ assertTrue(field.getAttributes().size() == 0);
+ assertNull(field.getStemming());
+ assertTrue(field.getNormalizing().doRemoveAccents());
+ assertTrue(field.isHeader());
+
+ // Second field
+ field=(SDField) document.getField("description");
+ assertEquals(RankType.ABOUT, field.getRankType());
+ assertEquals(SummaryTransform.NONE,
+ field.getSummaryField("description").getTransform());
+ assertEquals(SummaryTransform.DYNAMICTEASER,
+ field.getSummaryField("dyndesc").getTransform());
+ assertNull(field.getStemming());
+ assertTrue(field.getNormalizing().doRemoveAccents());
+ assertEquals("hallo",search.getIndex("description").aliasIterator().next());
+
+ // Third field
+ field=(SDField) document.getField("chatter");
+ assertEquals(RankType.ABOUT, field.getRankType());
+ assertNull(field.getStemming());
+ assertTrue(field.getNormalizing().doRemoveAccents());
+
+ // Fourth field
+ field=(SDField) document.getField("category");
+ assertEquals(0, field.getAttributes().size());
+ assertEquals(Stemming.NONE, field.getStemming());
+ assertTrue(!field.getNormalizing().doRemoveAccents());
+
+ // Fifth field
+ field=(SDField) document.getField("popularity");
+ assertEquals("{ attribute; }",
+ field.getIndexingScript().toString());
+
+ // Sixth field
+ field=(SDField) document.getField("measurement");
+ assertEquals(DataType.INT,field.getDataType());
+ assertEquals(RankType.EMPTY, field.getRankType());
+ assertEquals(1, field.getAttributes().size());
+
+ // Seventh field
+ field= search.getField("categories");
+ assertEquals("{ input categories_src | lowercase | normalize | index; }",
+ field.getIndexingScript().toString());
+ assertTrue(!field.isHeader());
+
+ // Eight field
+ field= search.getField("categoriesagain");
+ assertEquals("{ input categoriesagain_src | lowercase | normalize | index; }",
+ field.getIndexingScript().toString());
+ assertTrue(field.isHeader());
+
+ // Ninth field
+ field= search.getField("exactemento");
+ assertEquals("{ input exactemento_src | lowercase | index | summary; }",
+ field.getIndexingScript().toString());
+
+ // Tenth field
+ field = search.getField("category_arr");
+ assertEquals(1, field.getAttributes().size());
+ attribute = field.getAttributes().get("category_arr");
+ assertNotNull(attribute);
+ assertEquals("category_arr", attribute.getName());
+ assertEquals(Attribute.Type.STRING, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+ assertTrue(field.isHeader());
+
+ // Eleventh field
+ field = search.getField("measurement_arr");
+ assertEquals(1, field.getAttributes().size());
+ attribute = field.getAttributes().get("measurement_arr");
+ assertEquals("measurement_arr", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ // Rank Profiles
+ RankProfile profile=rankProfileRegistry.getRankProfile(search, "default");
+ assertNotNull(profile);
+ assertNull(profile.getInheritedName());
+ assertEquals(null,profile.getDeclaredRankSetting("measurement",
+ RankProfile.RankSetting.Type.RANKTYPE));
+ assertEquals(RankType.EMPTY,
+ profile.getRankSetting("measurement", RankProfile.RankSetting.Type.RANKTYPE).getValue());
+ profile=rankProfileRegistry.getRankProfile(search, "experimental");
+ assertNotNull(profile);
+ assertEquals("default",profile.getInheritedName());
+ assertEquals(RankType.IDENTITY,
+ profile.getDeclaredRankSetting("measurement", RankProfile.RankSetting.Type.RANKTYPE).getValue());
+
+ profile=rankProfileRegistry.getRankProfile(search, "other");
+ assertNotNull(profile);
+ assertEquals("experimental",profile.getInheritedName());
+
+ // The extra-document field
+ SDField exact=search.getField("exact");
+ assertNotNull("Extra field was parsed",exact);
+ assertEquals("exact",exact.getName());
+ assertEquals(Stemming.NONE,exact.getStemming());
+ assertTrue(!exact.getNormalizing().doRemoveAccents());
+ assertEquals("{ input title . \" \" . input category | summary | index; }",
+ exact.getIndexingScript().toString());
+ assertEquals(RankType.IDENTITY, exact.getRankType());
+ }
+
+ @Test
+ public void testDocumentImporting() throws IOException, ParseException {
+ try {
+ // Having two documents in one sd-file is illegal.
+ SearchBuilder.buildFromFile("src/test/examples/documents.sd");
+ fail();
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ @Test
+ public void testIdImporting() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/strange.sd");
+ SDField idecidemyide=(SDField) search.getDocument().getField("idecidemyide");
+ assertEquals(5,idecidemyide.getId(Document.SERIALIZED_VERSION));
+ SDField sodoi=(SDField) search.getDocument().getField("sodoi");
+ assertEquals(7,sodoi.getId(Document.SERIALIZED_VERSION));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/StemmingSettingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/StemmingSettingTestCase.java
new file mode 100644
index 00000000000..6a0dd6e2c1b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/StemmingSettingTestCase.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Level;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Stemming settings test
+ *
+ * @author bratseth
+ */
+public class StemmingSettingTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testStemmingSettings() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/stemmingsetting.sd");
+
+ SDField artist = (SDField)search.getDocument().getField("artist");
+ assertEquals(Stemming.SHORTEST, artist.getStemming(search));
+
+ SDField title = (SDField)search.getDocument().getField("title");
+ assertEquals(Stemming.NONE, title.getStemming(search));
+
+ SDField song = (SDField)search.getDocument().getField("song");
+ assertEquals(Stemming.MULTIPLE, song.getStemming(search));
+
+ SDField track = (SDField)search.getDocument().getField("track");
+ assertEquals(Stemming.SHORTEST, track.getStemming(search));
+
+ SDField backward = (SDField)search.getDocument().getField("backward");
+ assertEquals(Stemming.SHORTEST, backward.getStemming(search));
+
+ Index defaultIndex = search.getIndex("default");
+ assertEquals(Stemming.SHORTEST, defaultIndex.getStemming());
+ }
+
+ @Test
+ public void requireThatStemmingIsDefaultShortest() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/stemmingdefault.sd");
+ assertNull(search.getField("my_str").getStemming());
+ assertEquals(Stemming.SHORTEST, search.getField("my_str").getStemming(search));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/StructTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/StructTestCase.java
new file mode 100755
index 00000000000..0b119ccbf0d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/StructTestCase.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.document.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.searchdefinition.derived.Deriver;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+import java.io.IOException;
+import static org.junit.Assert.fail;
+
+/**
+ * tests importing of document containing array type fields
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class StructTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testStruct() throws IOException, ParseException {
+ assertConfigFile("src/test/examples/structresult.cfg",
+ new DocumentmanagerConfig(Deriver.getDocumentManagerConfig("src/test/examples/struct.sd")).toString() + "\n");
+ }
+ @Test
+ public void testBadStruct() throws IOException {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/badstruct.sd");
+ fail("Should throw exception.");
+ } catch (ParseException e) {
+ //ok!
+ //e.printStackTrace();
+ }
+ }
+ @Test
+ public void testStructAndDocumentWithSameNames() throws IOException, ParseException {
+ try {
+ DocumenttypesConfig.Builder dt = Deriver.getDocumentTypesConfig("src/test/examples/structanddocumentwithsamenames.sd");
+ } catch (Exception e) {
+ fail("Should not have thrown exception " + e.toString());
+ }
+ }
+
+ /**
+ * Declaring a struct before a document will fail, no doc type to add it to.
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testStructOutsideDocumentIllegal() throws IOException, ParseException {
+ SearchBuilder.buildFromFile("src/test/examples/structoutsideofdocument.sd");
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/UrlFieldValidationTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/UrlFieldValidationTestCase.java
new file mode 100644
index 00000000000..98409a01432
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/UrlFieldValidationTestCase.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+
+import static junit.framework.TestCase.fail;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ */
+public class UrlFieldValidationTestCase {
+
+ @Test
+ public void requireThatInheritedRiseFieldsStillCanBeInConflictButDontThrowException() throws ParseException {
+ SearchBuilder builder = new SearchBuilder();
+ builder.importString("search test {" +
+ " document test { " +
+ " field a type uri { indexing: attribute | summary }" +
+ " }" +
+ "}");
+ try {
+ builder.build();
+ fail("Should have caused an exception");
+ // success
+ } catch (IllegalArgumentException e) {
+ assertEquals("Error in field 'a' in search definition 'test': uri type fields cannot be attributes",
+ Exceptions.toMessageString(e));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/AbstractExportingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AbstractExportingTestCase.java
new file mode 100644
index 00000000000..bd7040e153b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AbstractExportingTestCase.java
@@ -0,0 +1,183 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.io.IOUtils;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.configmodel.producers.DocumentManager;
+import com.yahoo.vespa.configmodel.producers.DocumentTypes;
+
+import java.io.*;
+
+/**
+ * Superclass of tests needing file comparisons
+ *
+ * @author bratseth
+ */
+public abstract class AbstractExportingTestCase extends SearchDefinitionTestCase {
+
+ static final String tempDir = "temp/";
+ static final String searchDefRoot = "src/test/derived/";
+
+ private static final boolean WRITE_FILES = false;
+
+ static {
+ if ("true".equals(System.getProperty("sd.updatetests"))) {
+ /*
+ * Use this when you know that your code is correct, and don't want to manually check and fix the .cfg files
+ * in the exporting tests.
+ */
+ setUpUpdateTest();
+ }
+ // Or uncomment the lines you want below AND set WRITE_FILES to true
+ //System.setProperty("sd.updatetests", "true");
+ //System.setProperty("sd.updatetestfile", "attributes");
+ //System.setProperty("sd.updatetestfile", "documentmanager");
+ //System.setProperty("sd.updatetestfile", "fdispatchrc");
+ //System.setProperty("sd.updatetestfile", "ilscripts");
+ //System.setProperty("sd.updatetestfile", "index-info");
+ //System.setProperty("sd.updatetestfile", "indexschema");
+ //System.setProperty("sd.updatetestfile", "juniperrc");
+ //System.setProperty("sd.updatetestfile", "pan-rtx");
+ //System.setProperty("sd.updatetestfile", "pan-rtx-rtlogic");
+ //System.setProperty("sd.updatetestfile", "partitions");
+ //System.setProperty("sd.updatetestfile", "qr-logging");
+ //System.setProperty("sd.updatetestfile", "qr-searchers");
+ //System.setProperty("sd.updatetestfile", "rank-profiles");
+ //System.setProperty("sd.updatetestfile", "summary");
+ //System.setProperty("sd.updatetestfile", "summarymap");
+ //System.setProperty("sd.updatetestfile", "translogserver");
+ //System.setProperty("sd.updatetestfile", "vsmsummary");
+ //System.setProperty("sd.updatetestfile", "vsmfields");
+ }
+
+ private static void setUpUpdateTest() {
+ try {
+ System.out.println("Property sd.updatetests is true, updating test files from generated ones...");
+ System.out.println("Enter export test file name to be updated (eg. index-info), or blank to update every file. 'q' to quit: ");
+ String fileName = new BufferedReader(new InputStreamReader(System.in)).readLine();
+ if ("q".equals(fileName)) {
+ throw new IllegalArgumentException("Aborted by user");
+ }
+ System.setProperty("sd.updatetestfile", fileName);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ protected DerivedConfiguration derive(String dirName, String searchDefinitionName) throws IOException, ParseException {
+ File toDir = new File(tempDir + dirName);
+ toDir.mkdirs();
+ deleteContent(toDir);
+
+ SearchBuilder builder = SearchBuilder.createFromDirectory(searchDefRoot + dirName + "/");
+ //SearchBuilder builder = SearchBuilder.createFromFile(searchDefDir + name + ".sd");
+ return derive(dirName, searchDefinitionName, builder);
+ }
+
+ protected DerivedConfiguration derive(String dirName, String searchDefinitionName, SearchBuilder builder) throws IOException {
+ DerivedConfiguration config = new DerivedConfiguration(builder.getSearch(searchDefinitionName), builder.getRankProfileRegistry());
+ return export(dirName, builder, config);
+ }
+
+ protected DerivedConfiguration derive(String dirName, SearchBuilder builder, Search search) throws IOException {
+ DerivedConfiguration config = new DerivedConfiguration(search, builder.getRankProfileRegistry());
+ return export(dirName, builder, config);
+ }
+
+ private DerivedConfiguration export(String name, SearchBuilder builder, DerivedConfiguration config) throws IOException {
+ String path = exportConfig(name, config);
+ DerivedConfiguration.exportDocuments(new DocumentManager().produce(builder.getModel(), new DocumentmanagerConfig.Builder()), path);
+ DerivedConfiguration.exportDocuments(new DocumentTypes().produce(builder.getModel(), new DocumenttypesConfig.Builder()), path);
+ return config;
+ }
+
+ private String exportConfig(String name, DerivedConfiguration config) throws IOException {
+ String path = tempDir + name;
+ config.export(path);
+ return path;
+ }
+
+ /**
+ * Derives a config from name/name.sd below the test dir and verifies that every .cfg file in name/ has a
+ * corresponding file with the same content in temp/name. Versions can and should be omitted from the .cfg file
+ * names. This will fail if the search definition dir has multiple search definitions.
+ *
+ * @param dirName the name of the directory containing the searchdef file to verify.
+ * @throws ParseException if the .sd file could not be parsed.
+ * @throws IOException if file access failed.
+ */
+ protected DerivedConfiguration assertCorrectDeriving(String dirName) throws IOException, ParseException {
+ return assertCorrectDeriving(dirName, null);
+ }
+
+ protected DerivedConfiguration assertCorrectDeriving(String dirName, String searchDefinitionName) throws IOException, ParseException {
+ DerivedConfiguration derived = derive(dirName, searchDefinitionName);
+ assertCorrectConfigFiles(dirName);
+ return derived;
+ }
+
+ /**
+ * Asserts config is correctly derived given a builder.
+ * This will fail if the builder contains multiple search definitions.
+ */
+ protected DerivedConfiguration assertCorrectDeriving(SearchBuilder builder, String dirName) throws IOException, ParseException {
+ builder.build();
+ DerivedConfiguration derived = derive(dirName, null, builder);
+ assertCorrectConfigFiles(dirName);
+ return derived;
+ }
+
+ protected DerivedConfiguration assertCorrectDeriving(SearchBuilder builder, Search search, String name) throws IOException, ParseException {
+ DerivedConfiguration derived = derive(name, builder, search);
+ assertCorrectConfigFiles(name);
+ return derived;
+ }
+
+ /**
+ * Assert that search is derived into the files in the directory given by name.
+ *
+ * @param name the local name of the directory containing the files to check
+ * @throws IOException If file access failed.
+ */
+ protected void assertCorrectConfigFiles(String name) throws IOException {
+ File[] files = new File(searchDefRoot, name).listFiles();
+ if (files == null) return;
+ for (File file : files) {
+ if ( ! file.getName().endsWith(".cfg")) continue;
+ assertEqualFiles(file.getPath(), tempDir + name + "/" + file.getName());
+ }
+ }
+
+ protected void assertEqualConfig(String name, String config) throws IOException {
+ final String expectedConfigDirName = searchDefRoot + name + "/";
+ assertEqualFiles(expectedConfigDirName + config + ".cfg",
+ tempDir + name + "/" + config + ".cfg");
+ }
+
+ @SuppressWarnings({ "ConstantConditions" })
+ public static void assertEqualFiles(String correctFileName, String checkFileName) throws IOException {
+ if (WRITE_FILES || "true".equals(System.getProperty("sd.updatetests"))) {
+ String updateFile = System.getProperty("sd.updatetestfile");
+ if (WRITE_FILES || "".equals(updateFile) || correctFileName.endsWith(updateFile + ".cfg") || correctFileName.endsWith(updateFile + ".MODEL.cfg")) {
+ System.out.println("Copying " + checkFileName + " to " + correctFileName);
+ IOUtils.copy(checkFileName, correctFileName);
+ return;
+ }
+ }
+ assertConfigFiles(correctFileName, checkFileName);
+ }
+
+ protected void deleteContent(File dir) {
+ File[] files = dir.listFiles();
+ if (files == null) return;
+
+ for (File file : files) {
+ file.delete();
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/AnnotationsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AnnotationsTestCase.java
new file mode 100755
index 00000000000..c1b1101961a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AnnotationsTestCase.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class AnnotationsTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void requireThatStructRegistersIfOnlyUsedByAnnotation() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsstruct");
+ }
+
+ @Test
+ public void requireThatStructRegistersIfOnlyUsedAsArrayByAnnotation() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsstructarray");
+ }
+
+ @Test
+ public void testSimpleAnnotationDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("annotationssimple");
+ }
+
+ @Test
+ public void testAnnotationDerivingWithImplicitStruct() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsimplicitstruct");
+ }
+
+ @Test
+ public void testAnnotationDerivingInheritance() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsinheritance");
+ }
+
+ @Test
+ public void testAnnotationDerivingInheritance2() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsinheritance2");
+ }
+
+ @Test
+ public void testSimpleReference() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsreference");
+ }
+
+ @Test
+ public void testAdvancedReference() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsreference2");
+ }
+
+ @Test
+ public void testAnnotationsPolymorphy() throws IOException, ParseException {
+ assertCorrectDeriving("annotationspolymorphy");
+ }
+
+ /**
+ * An annotation declared before document {} won't work, no doc type to add it to.
+ * @throws IOException
+ * @throws ParseException
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testAnnotationOutsideOfDocumment() throws IOException, ParseException {
+ assertCorrectDeriving("annotationsoutsideofdocument");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/ArraysTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/ArraysTestCase.java
new file mode 100644
index 00000000000..461dfea5472
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/ArraysTestCase.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests array type deriving. Indexing statements over array
+ * types is not yet supported, so this tests document type
+ * configuration deriving only. Expand later.
+ *
+ * @author bratseth
+ */
+public class ArraysTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocumentDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("arrays");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributeListTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributeListTestCase.java
new file mode 100644
index 00000000000..09899f4796f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributeListTestCase.java
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests attribute deriving
+ *
+ * @author bratseth
+ */
+public class AttributeListTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+
+ // Test attribute importing
+ Search search = SearchBuilder.buildFromFile("src/test/examples/simple.sd");
+
+ // Test attribute deriving
+ AttributeFields attributeFields = new AttributeFields(search);
+ Iterator attributes = attributeFields.attributeIterator();
+ Attribute attribute;
+ attribute = (Attribute)attributes.next();
+ assertEquals("popularity", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("measurement", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("smallattribute", attribute.getName());
+ assertEquals(Attribute.Type.BYTE, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("access", attribute.getName());
+ assertEquals(Attribute.Type.BYTE, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("category_arr", attribute.getName());
+ assertEquals(Attribute.Type.STRING, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("measurement_arr", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.ARRAY, attribute.getCollectionType());
+
+ attribute = (Attribute)attributes.next();
+ assertEquals("popsiness", attribute.getName());
+ assertEquals(Attribute.Type.INTEGER, attribute.getType());
+ assertEquals(Attribute.CollectionType.SINGLE, attribute.getCollectionType());
+
+ assertTrue(!attributes.hasNext());
+
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributesTestCase.java
new file mode 100644
index 00000000000..0947671e5c7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/AttributesTestCase.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests attribute settings
+ *
+ * @author bratseth
+ */
+public class AttributesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testDocumentDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("attributes");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/CasingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/CasingTestCase.java
new file mode 100644
index 00000000000..92469c69fa4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/CasingTestCase.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Correct casing for derived attributes
+ *
+ * @author vegardh
+ */
+public class CasingTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testCasing() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/casing.sd");
+ assertEquals(search.getIndex("color").getName(), "color");
+ assertEquals(search.getIndex("Foo").getName(), "Foo");
+ assertEquals(search.getIndex("Price").getName(), "Price");
+ assertEquals(search.getAttribute("artist").getName(), "artist");
+ assertEquals(search.getAttribute("Drummer").getName(), "Drummer");
+ assertEquals(search.getAttribute("guitarist").getName(), "guitarist");
+ assertEquals(search.getAttribute("title").getName(), "title");
+ assertEquals(search.getAttribute("Trumpetist").getName(), "Trumpetist");
+ assertEquals(search.getAttribute("Saxophonist").getName(), "Saxophonist");
+ assertEquals(search.getAttribute("TenorSaxophonist").getName(), "TenorSaxophonist");
+ assertEquals(search.getAttribute("Flutist").getName(), "Flutist");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/CombinedAttributeAndIndexSearchTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/CombinedAttributeAndIndexSearchTestCase.java
new file mode 100644
index 00000000000..95d3dfac3f2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/CombinedAttributeAndIndexSearchTestCase.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving a configuration with multiple summaries
+ *
+ * @author bratseth
+ */
+public class CombinedAttributeAndIndexSearchTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testMultipleSummaries() throws IOException, ParseException {
+ assertCorrectDeriving("combinedattributeandindexsearch");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/DeriverTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/DeriverTestCase.java
new file mode 100644
index 00000000000..ea18fcb5266
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/DeriverTestCase.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.io.IOUtils;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import org.junit.Test;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests deriving using the Deriver facade
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class DeriverTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testDeriveDocManager() {
+ DocumentTypeManager dtm = new DocumentTypeManager(new DocumentmanagerConfig(
+ Deriver.getDocumentManagerConfig(new ArrayList<String>()
+ {{ add("src/test/derived/deriver/child.sd");
+ add("src/test/derived/deriver/parent.sd");
+ add("src/test/derived/deriver/grandparent.sd");}})));
+ assertEquals(dtm.getDocumentType("child").getField("a").getDataType(), DataType.STRING);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/DocumentDeriverTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/DocumentDeriverTestCase.java
new file mode 100644
index 00000000000..6c08fb88870
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/DocumentDeriverTestCase.java
@@ -0,0 +1,114 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests deriving of documentmanager
+ *
+ * @author <a href="mailto:mathiasm@yahoo-inc.com">Mathias Moelster Lidal</a>
+ */
+public class DocumentDeriverTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testDocumentDeriving() {
+ String root = "src/test/derived/documentderiver/";
+
+ List<String> files = new ArrayList<>();
+ files.add(root + "newsarticle.sd");
+ files.add(root + "newssummary.sd");
+ files.add(root + "music.sd");
+ files.add(root + "mail.sd");
+ files.add(root + "compression_header.sd");
+ files.add(root + "compression_both.sd");
+ files.add(root + "compression_body.sd");
+
+ File toDir = new File("temp/documentderiver/");
+ toDir.mkdir();
+
+ SearchBuilder builder = Deriver.deriveDocuments(files, toDir.getPath());
+ try {
+ assertEqualFiles(root + "documentmanager.cfg", toDir.getPath() + "/documentmanager.cfg");
+ } catch (IOException e) {
+ throw new RuntimeException("Exception while comparing files", e);
+ }
+
+ SDDocumentType doc = builder.getSearch("newsarticle").getDocument();
+ assertNotNull(doc);
+ }
+ @Test
+ public void testStructTypesNotUsed() {
+ String root = "src/test/derived/documentderiver/";
+
+ List<String> files = new ArrayList<>();
+ files.add(root + "sombrero.sd");
+
+ File toDir = new File("temp/structtypesnotused/");
+ toDir.mkdir();
+
+ Deriver.deriveDocuments(files, toDir.getPath());
+
+ DocumentTypeManager dtm = new DocumentTypeManager();
+ int numBuiltInTypes = dtm.getDataTypes().size();
+ dtm.configure("file:" + toDir.getPath() + "/documentmanager.cfg");
+
+ DocumentType webDocType = dtm.getDocumentType("webdoc");
+ assertNotNull(webDocType);
+
+ assertEquals(1, webDocType.fieldSet().size());
+ Field html = webDocType.getField("html");
+ assertNotNull(html);
+ assertEquals(DataType.STRING, html.getDataType());
+
+ assertEquals(numBuiltInTypes + 8, dtm.getDataTypes().size());
+
+ {
+ StructDataType keyvalue = (StructDataType) dtm.getDataType("keyvalue");
+ assertNotNull(keyvalue);
+ assertEquals(2, keyvalue.getFields().size());
+ Field key = keyvalue.getField("key");
+ assertNotNull(key);
+ assertEquals(DataType.STRING, key.getDataType());
+ Field value = keyvalue.getField("value");
+ assertNotNull(value);
+ assertEquals(DataType.STRING, value.getDataType());
+ }
+ {
+ StructDataType tagvalue = (StructDataType) dtm.getDataType("tagvalue");
+ assertNotNull(tagvalue);
+ assertEquals(2, tagvalue.getFields().size());
+ Field name = tagvalue.getField("name");
+ assertNotNull(name);
+ assertEquals(DataType.STRING, name.getDataType());
+ Field attributes = tagvalue.getField("attributes");
+ assertNotNull(attributes);
+ assertTrue(attributes.getDataType() instanceof ArrayDataType);
+ assertEquals(dtm.getDataType("keyvalue"), ((ArrayDataType) attributes.getDataType()).getNestedType());
+ }
+ {
+ StructDataType wordform = (StructDataType) dtm.getDataType("wordform");
+ assertNotNull(wordform);
+ assertEquals(3, wordform.getFields().size());
+ Field kind = wordform.getField("kind");
+ assertNotNull(kind);
+ assertEquals(DataType.INT, kind.getDataType());
+ Field form = wordform.getField("form");
+ assertNotNull(form);
+ assertEquals(DataType.STRING, form.getDataType());
+ Field weight = wordform.getField("weight");
+ assertNotNull(weight);
+ assertEquals(DataType.FLOAT, weight.getDataType());
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/EmptyRankProfileTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/EmptyRankProfileTestCase.java
new file mode 100644
index 00000000000..57d62326fe3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/EmptyRankProfileTestCase.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving rank for files from search definitions
+ *
+ * @author <a href="mailto:Jon S Bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class EmptyRankProfileTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+ Search search = new Search("test", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+ SDDocumentType doc = new SDDocumentType("test");
+ search.addDocument(doc);
+ doc.addField(new SDField("a", DataType.STRING));
+ SDField field = new SDField("b", DataType.STRING);
+ field.setLiteralBoost(500);
+ doc.addField(field);
+ doc.addField(new SDField("c", DataType.STRING));
+
+ search = SearchBuilder.buildFromRawSearch(search, rankProfileRegistry);
+ new DerivedConfiguration(search, rankProfileRegistry);
+ }
+} \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/ExactMatchTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/ExactMatchTestCase.java
new file mode 100644
index 00000000000..06729b1e27b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/ExactMatchTestCase.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ExactMatchTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testExactString() throws IOException, ParseException {
+ assertCorrectDeriving("exactmatch");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/ExportingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/ExportingTestCase.java
new file mode 100644
index 00000000000..c93e07d57c1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/ExportingTestCase.java
@@ -0,0 +1,142 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests exporting
+ *
+ * @author bratseth
+ */
+public class ExportingTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testIndexInfoLowerCase() throws IOException, ParseException {
+ assertCorrectDeriving("indexinfo_lowercase");
+ }
+
+ @Test
+ public void testPositionArray() throws IOException, ParseException {
+ assertCorrectDeriving("position_array");
+ }
+
+ @Test
+ public void testPositionAttribute() throws IOException, ParseException {
+ assertCorrectDeriving("position_attribute");
+ }
+
+ @Test
+ public void testPositionExtra() throws IOException, ParseException {
+ assertCorrectDeriving("position_extra");
+ }
+
+ @Test
+ public void testPositionNoSummary() throws IOException, ParseException {
+ assertCorrectDeriving("position_nosummary");
+ }
+
+ @Test
+ public void testPositionSummary() throws IOException, ParseException {
+ assertCorrectDeriving("position_summary");
+ }
+
+ @Test
+ public void testUriArray() throws IOException, ParseException {
+ assertCorrectDeriving("uri_array");
+ }
+
+ @Test
+ public void testUriWSet() throws IOException, ParseException {
+ assertCorrectDeriving("uri_wset");
+ }
+
+ @Test
+ public void testMusic() throws IOException, ParseException {
+ assertCorrectDeriving("music");
+ }
+
+ @Test
+ public void testComplexPhysicalExporting() throws IOException, ParseException {
+ assertCorrectDeriving("complex");
+ }
+
+ @Test
+ public void testAttributePrefetch() throws IOException, ParseException {
+ assertCorrectDeriving("attributeprefetch");
+ }
+
+ @Test
+ public void testAdvancedIL() throws IOException, ParseException {
+ assertCorrectDeriving("advanced");
+ }
+
+ @Test
+ public void testEmptyDefaultIndex() throws IOException, ParseException {
+ assertCorrectDeriving("emptydefault");
+ }
+
+ @Test
+ public void testIndexSwitches() throws IOException, ParseException {
+ assertCorrectDeriving("indexswitches");
+ }
+
+ @Test
+ public void testRankTypes() throws IOException, ParseException {
+ assertCorrectDeriving("ranktypes");
+ }
+
+ @Test
+ public void testAttributeRank() throws IOException, ParseException {
+ assertCorrectDeriving("attributerank");
+ }
+
+ @Test
+ public void testNewRank() throws IOException, ParseException {
+ assertCorrectDeriving("newrank");
+ }
+
+ @Test
+ public void testRankExpression() throws IOException, ParseException {
+ assertCorrectDeriving("rankexpression");
+ }
+
+ @Test
+ public void testMlr() throws IOException, ParseException {
+ assertCorrectDeriving("mlr");
+ }
+
+ @Test
+ public void testMusic3() throws IOException, ParseException {
+ assertCorrectDeriving("music3");
+ }
+
+ @Test
+ public void testIndexSchema() throws IOException, ParseException {
+ assertCorrectDeriving("indexschema");
+ }
+
+ @Test
+ public void testIndexinfoFieldsets() throws IOException, ParseException {
+ assertCorrectDeriving("indexinfo_fieldsets");
+ }
+
+ @Test
+ public void testStreamingJuniper() throws IOException, ParseException {
+ assertCorrectDeriving("streamingjuniper");
+ }
+
+ @Test
+ public void testPredicateAttribute() throws IOException, ParseException {
+ assertCorrectDeriving("predicate_attribute");
+ }
+
+ @Test
+ public void testTensorField() throws IOException, ParseException {
+ assertCorrectDeriving("tensor");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/GeminiTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/GeminiTestCase.java
new file mode 100644
index 00000000000..9006cfc73de
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/GeminiTestCase.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author bratseth
+ */
+public class GeminiTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testRanking2() throws IOException, ParseException {
+ DerivedConfiguration c = assertCorrectDeriving("gemini2");
+ RawRankProfile p = c.getRankProfileList().getRankProfile("test");
+ Map<String, String> ranking = removePartKeySuffixes(p.configProperties());
+ assertEquals("attribute(right)", resolve(lookup("toplevel", ranking), ranking));
+ }
+
+ private Map<String, String> removePartKeySuffixes(Map<String, Object> p) {
+ Map<String, String> pWithoutSuffixes = new HashMap<>();
+ for (Map.Entry<String, Object> entry : p.entrySet())
+ pWithoutSuffixes.put(removePartSuffix(entry.getKey()), entry.getValue().toString());
+ return pWithoutSuffixes;
+ }
+
+ private String removePartSuffix(String s) {
+ int partIndex = s.indexOf(".part");
+ if (partIndex <= 0) return s;
+ return s.substring(0, partIndex);
+ }
+
+ /**
+ * Recurively resolves references to other ranking expressions - rankingExpression(name) -
+ * and replaces the reference by the expression
+ */
+ private String resolve(String expression, Map<String, String> ranking) {
+ int referenceStartIndex;
+ while ((referenceStartIndex = expression.indexOf("rankingExpression(")) >= 0) {
+ int referenceEndIndex = expression.indexOf(")", referenceStartIndex);
+ expression = expression.substring(0, referenceStartIndex) +
+ resolve(lookup(expression.substring(referenceStartIndex + "rankingExpression(".length(), referenceEndIndex), ranking), ranking) +
+ expression.substring(referenceEndIndex + 1);
+ }
+ return expression;
+ }
+
+ private String lookup(String expressionName, Map<String, String> ranking) {
+ String value = ranking.get("rankingExpression(" + expressionName + ").rankingScript");
+ if (value == null) {
+ System.out.println("Warning: No expression found for " + expressionName);
+ return expressionName;
+ }
+ return value;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/IdTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/IdTestCase.java
new file mode 100644
index 00000000000..ca5884accd5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/IdTestCase.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.processing.Processing;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Tests that documents ids are treated as they should
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class IdTestCase extends AbstractExportingTestCase {
+
+ @Test
+ @SuppressWarnings({ "deprecation" })
+ public void testExplicitUpperCaseIdField() {
+ Search search = new Search("test", null);
+ SDDocumentType document = new SDDocumentType("test");
+ search.addDocument(document);
+ SDField uri = new SDField("URI", DataType.URI);
+ uri.parseIndexingScript("{ summary | index }");
+ document.addField(uri);
+
+ Processing.process(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles());
+
+ assertNull(document.getField("uri"));
+ assertNull(document.getField("Uri"));
+ assertNotNull(document.getField("URI"));
+ }
+
+ @Test
+ public void testCompleteDeriving() throws Exception {
+ assertCorrectDeriving("id");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/IndexSchemaTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/IndexSchemaTestCase.java
new file mode 100644
index 00000000000..f1547c9b09e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/IndexSchemaTestCase.java
@@ -0,0 +1,209 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.Field;
+import com.yahoo.document.StructDataType;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+@SuppressWarnings({ "deprecation" })
+public class IndexSchemaTestCase {
+
+ @Test
+ public void requireThatPrimitiveIsNotFlattened() {
+ assertFlat(new Field("foo", DataType.BYTE), new Field("foo", DataType.BYTE));
+ assertFlat(new Field("foo", DataType.DOUBLE), new Field("foo", DataType.DOUBLE));
+ assertFlat(new Field("foo", DataType.FLOAT), new Field("foo", DataType.FLOAT));
+ assertFlat(new Field("foo", DataType.INT), new Field("foo", DataType.INT));
+ assertFlat(new Field("foo", DataType.LONG), new Field("foo", DataType.LONG));
+ assertFlat(new Field("foo", DataType.RAW), new Field("foo", DataType.RAW));
+ assertFlat(new Field("foo", DataType.STRING), new Field("foo", DataType.STRING));
+ assertFlat(new Field("foo", DataType.URI), new Field("foo", DataType.URI));
+ assertFlat(new Field("foo", DataType.PREDICATE), new Field("foo", DataType.PREDICATE));
+ }
+
+ @Test
+ public void requireThatArrayOfPrimitiveIsNotFlattened() {
+ assertFlat(new Field("foo", DataType.getArray(DataType.BYTE)),
+ new Field("foo", DataType.getArray(DataType.BYTE)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.DOUBLE)),
+ new Field("foo", DataType.getArray(DataType.DOUBLE)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.FLOAT)),
+ new Field("foo", DataType.getArray(DataType.FLOAT)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.INT)),
+ new Field("foo", DataType.getArray(DataType.INT)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.LONG)),
+ new Field("foo", DataType.getArray(DataType.LONG)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.RAW)),
+ new Field("foo", DataType.getArray(DataType.RAW)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.STRING)),
+ new Field("foo", DataType.getArray(DataType.STRING)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.URI)),
+ new Field("foo", DataType.getArray(DataType.URI)));
+ assertFlat(new Field("foo", DataType.getArray(DataType.PREDICATE)),
+ new Field("foo", DataType.getArray(DataType.PREDICATE)));
+ }
+
+ @Test
+ public void requireThatStructIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.BYTE));
+ type.addField(new Field("my_double", DataType.DOUBLE));
+ type.addField(new Field("my_float", DataType.FLOAT));
+ type.addField(new Field("my_int", DataType.INT));
+ type.addField(new Field("my_long", DataType.LONG));
+ type.addField(new Field("my_raw", DataType.RAW));
+ type.addField(new Field("my_string", DataType.STRING));
+ type.addField(new Field("my_uri", DataType.URI));
+
+ assertFlat(new Field("foo", type),
+ new Field("foo.my_byte", DataType.BYTE),
+ new Field("foo.my_double", DataType.DOUBLE),
+ new Field("foo.my_float", DataType.FLOAT),
+ new Field("foo.my_int", DataType.INT),
+ new Field("foo.my_long", DataType.LONG),
+ new Field("foo.my_raw", DataType.RAW),
+ new Field("foo.my_string", DataType.STRING),
+ new Field("foo.my_uri", DataType.URI));
+ }
+
+ @Test
+ public void requireThatArrayOfStructIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.BYTE));
+ type.addField(new Field("my_double", DataType.DOUBLE));
+ type.addField(new Field("my_float", DataType.FLOAT));
+ type.addField(new Field("my_int", DataType.INT));
+ type.addField(new Field("my_long", DataType.LONG));
+ type.addField(new Field("my_raw", DataType.RAW));
+ type.addField(new Field("my_string", DataType.STRING));
+ type.addField(new Field("my_uri", DataType.URI));
+
+ assertFlat(new Field("foo", DataType.getArray(type)),
+ new Field("foo.my_byte", DataType.getArray(DataType.BYTE)),
+ new Field("foo.my_double", DataType.getArray(DataType.DOUBLE)),
+ new Field("foo.my_float", DataType.getArray(DataType.FLOAT)),
+ new Field("foo.my_int", DataType.getArray(DataType.INT)),
+ new Field("foo.my_long", DataType.getArray(DataType.LONG)),
+ new Field("foo.my_raw", DataType.getArray(DataType.RAW)),
+ new Field("foo.my_string", DataType.getArray(DataType.STRING)),
+ new Field("foo.my_uri", DataType.getArray(DataType.URI)));
+ }
+
+ @Test
+ public void requireThatArrayOfArrayOfStructIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.BYTE));
+ type.addField(new Field("my_double", DataType.DOUBLE));
+ type.addField(new Field("my_float", DataType.FLOAT));
+ type.addField(new Field("my_int", DataType.INT));
+ type.addField(new Field("my_long", DataType.LONG));
+ type.addField(new Field("my_raw", DataType.RAW));
+ type.addField(new Field("my_string", DataType.STRING));
+ type.addField(new Field("my_uri", DataType.URI));
+
+ assertFlat(new Field("foo", DataType.getArray(DataType.getArray(type))),
+ new Field("foo.my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))),
+ new Field("foo.my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))),
+ new Field("foo.my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))),
+ new Field("foo.my_int", DataType.getArray(DataType.getArray(DataType.INT))),
+ new Field("foo.my_long", DataType.getArray(DataType.getArray(DataType.LONG))),
+ new Field("foo.my_raw", DataType.getArray(DataType.getArray(DataType.RAW))),
+ new Field("foo.my_string", DataType.getArray(DataType.getArray(DataType.STRING))),
+ new Field("foo.my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+ }
+
+ @Test
+ public void requireThatStructWithArrayFieldIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.getArray(DataType.BYTE)));
+ type.addField(new Field("my_double", DataType.getArray(DataType.DOUBLE)));
+ type.addField(new Field("my_float", DataType.getArray(DataType.FLOAT)));
+ type.addField(new Field("my_int", DataType.getArray(DataType.INT)));
+ type.addField(new Field("my_long", DataType.getArray(DataType.LONG)));
+ type.addField(new Field("my_raw", DataType.getArray(DataType.RAW)));
+ type.addField(new Field("my_string", DataType.getArray(DataType.STRING)));
+ type.addField(new Field("my_uri", DataType.getArray(DataType.URI)));
+
+ assertFlat(new Field("foo", type),
+ new Field("foo.my_byte", DataType.getArray(DataType.BYTE)),
+ new Field("foo.my_double", DataType.getArray(DataType.DOUBLE)),
+ new Field("foo.my_float", DataType.getArray(DataType.FLOAT)),
+ new Field("foo.my_int", DataType.getArray(DataType.INT)),
+ new Field("foo.my_long", DataType.getArray(DataType.LONG)),
+ new Field("foo.my_raw", DataType.getArray(DataType.RAW)),
+ new Field("foo.my_string", DataType.getArray(DataType.STRING)),
+ new Field("foo.my_uri", DataType.getArray(DataType.URI)));
+ }
+
+ @Test
+ public void requireThatStructWithArrayOfArrayFieldIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))));
+ type.addField(new Field("my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))));
+ type.addField(new Field("my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))));
+ type.addField(new Field("my_int", DataType.getArray(DataType.getArray(DataType.INT))));
+ type.addField(new Field("my_long", DataType.getArray(DataType.getArray(DataType.LONG))));
+ type.addField(new Field("my_raw", DataType.getArray(DataType.getArray(DataType.RAW))));
+ type.addField(new Field("my_string", DataType.getArray(DataType.getArray(DataType.STRING))));
+ type.addField(new Field("my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+
+ assertFlat(new Field("foo", type),
+ new Field("foo.my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))),
+ new Field("foo.my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))),
+ new Field("foo.my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))),
+ new Field("foo.my_int", DataType.getArray(DataType.getArray(DataType.INT))),
+ new Field("foo.my_long", DataType.getArray(DataType.getArray(DataType.LONG))),
+ new Field("foo.my_raw", DataType.getArray(DataType.getArray(DataType.RAW))),
+ new Field("foo.my_string", DataType.getArray(DataType.getArray(DataType.STRING))),
+ new Field("foo.my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+ }
+
+ @Test
+ public void requireThatArrayOfStructWithArrayFieldIsFlattened() {
+ StructDataType type = new StructDataType("my_struct");
+ type.addField(new Field("my_byte", DataType.getArray(DataType.BYTE)));
+ type.addField(new Field("my_double", DataType.getArray(DataType.DOUBLE)));
+ type.addField(new Field("my_float", DataType.getArray(DataType.FLOAT)));
+ type.addField(new Field("my_int", DataType.getArray(DataType.INT)));
+ type.addField(new Field("my_long", DataType.getArray(DataType.LONG)));
+ type.addField(new Field("my_raw", DataType.getArray(DataType.RAW)));
+ type.addField(new Field("my_string", DataType.getArray(DataType.STRING)));
+ type.addField(new Field("my_uri", DataType.getArray(DataType.URI)));
+
+ assertFlat(new Field("foo", DataType.getArray(type)),
+ new Field("foo.my_byte", DataType.getArray(DataType.getArray(DataType.BYTE))),
+ new Field("foo.my_double", DataType.getArray(DataType.getArray(DataType.DOUBLE))),
+ new Field("foo.my_float", DataType.getArray(DataType.getArray(DataType.FLOAT))),
+ new Field("foo.my_int", DataType.getArray(DataType.getArray(DataType.INT))),
+ new Field("foo.my_long", DataType.getArray(DataType.getArray(DataType.LONG))),
+ new Field("foo.my_raw", DataType.getArray(DataType.getArray(DataType.RAW))),
+ new Field("foo.my_string", DataType.getArray(DataType.getArray(DataType.STRING))),
+ new Field("foo.my_uri", DataType.getArray(DataType.getArray(DataType.URI))));
+ }
+
+ private static void assertFlat(Field fieldToFlatten, Field... expectedFields) {
+ List<Field> actual = new LinkedList<>(IndexSchema.flattenField(fieldToFlatten));
+ List<Field> expected = new LinkedList<>(Arrays.asList(expectedFields));
+ Collections.sort(actual);
+ Collections.sort(expected);
+ for (Field field : actual) {
+ if (!expected.remove(field)) {
+ fail("Unexpected field: " + field);
+ }
+ }
+ assertTrue("Missing fields: " + expected, expected.isEmpty());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/InheritanceTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/InheritanceTestCase.java
new file mode 100644
index 00000000000..cb35062e59e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/InheritanceTestCase.java
@@ -0,0 +1,179 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.configmodel.producers.DocumentManager;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests inheritance
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class InheritanceTestCase extends AbstractExportingTestCase {
+
+ @Rule
+ public TemporaryFolder tmpDir = new TemporaryFolder();
+
+ @Test
+ public void requireThatIndexedStructFieldCanBeInherited() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritstruct/";
+ SearchBuilder builder = new SearchBuilder();
+ builder.importFile(dir + "parent.sd");
+ builder.importFile(dir + "child.sd");
+ builder.build();
+ derive("inheritstruct", builder, builder.getSearch("child"));
+ assertCorrectConfigFiles("inheritstruct");
+ }
+
+ @Test
+ public void requireThatInheritFromNullIsCaught() throws IOException, ParseException {
+ try {
+ assertCorrectDeriving("inheritfromnull");
+ } catch (IllegalStateException e) {
+ assertEquals("Document type 'foo' not found.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatStructTypesAreInheritedThroughDiamond() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritdiamond/";
+ List<String> files = Arrays.asList("grandparent.sd", "mother.sd", "father.sd", "child.sd");
+ File outDir = tmpDir.newFolder("out");
+ for (int startIdx = 0; startIdx < files.size(); ++startIdx) {
+ SearchBuilder builder = new SearchBuilder();
+ for (int fileIdx = startIdx; fileIdx < startIdx + files.size(); ++fileIdx) {
+ String fileName = files.get(fileIdx % files.size());
+ builder.importFile(dir + fileName);
+ }
+ builder.build();
+ DocumentmanagerConfig.Builder b = new DocumentmanagerConfig.Builder();
+ DerivedConfiguration.exportDocuments(new DocumentManager().produce(builder.getModel(), b), outDir.getPath());
+ DocumentmanagerConfig dc = new DocumentmanagerConfig(b);
+ assertEquals(17, dc.datatype().size());
+ assertNotNull(structType("child.body", dc));
+ DocumentmanagerConfig.Datatype.Structtype childHeader = structType("child.header", dc);
+ assertEquals(childHeader.field(0).name(), "foo");
+ assertEquals(childHeader.field(1).name(), "bar");
+ assertEquals(childHeader.field(2).name(), "baz");
+ assertEquals(childHeader.field(3).name(), "cox");
+ DocumentmanagerConfig.Datatype.Documenttype child = documentType("child", dc);
+ assertEquals(child.inherits(0).name(), "document");
+ assertEquals(child.inherits(1).name(), "father");
+ assertEquals(child.inherits(2).name(), "mother");
+ DocumentmanagerConfig.Datatype.Documenttype mother = documentType("mother", dc);
+ assertEquals(mother.inherits(0).name(), "grandparent");
+ assertEquals(mother.inherits(1).name(), "document");
+ }
+ }
+
+ private DocumentmanagerConfig.Datatype.Structtype structType(String name, DocumentmanagerConfig dc) {
+ for (DocumentmanagerConfig.Datatype dt : dc.datatype()) {
+ for (DocumentmanagerConfig.Datatype.Structtype st : dt.structtype()) {
+ if (name.equals(st.name())) return st;
+ }
+ }
+ return null;
+ }
+
+ private DocumentmanagerConfig.Datatype.Documenttype documentType(String name, DocumentmanagerConfig dc) {
+ for (DocumentmanagerConfig.Datatype dt : dc.datatype()) {
+ for (DocumentmanagerConfig.Datatype.Documenttype dot : dt.documenttype()) {
+ if (name.equals(dot.name())) return dot;
+ }
+ }
+ return null;
+ }
+
+ @Test
+ public void requireThatStructTypesAreInheritedFromParent() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritfromparent/";
+ SearchBuilder builder = new SearchBuilder();
+ builder.importFile(dir + "parent.sd");
+ builder.importFile(dir + "child.sd");
+ builder.build();
+ derive("inheritfromparent", builder, builder.getSearch("child"));
+ assertCorrectConfigFiles("inheritfromparent");
+ }
+
+ @Test
+ public void requireThatStructTypesAreInheritedFromGrandParent() throws IOException, ParseException {
+ String dir = "src/test/derived/inheritfromgrandparent/";
+ SearchBuilder builder = new SearchBuilder();
+ builder.importFile(dir + "grandparent.sd");
+ builder.importFile(dir + "parent.sd");
+ builder.importFile(dir + "child.sd");
+ builder.build();
+ derive("inheritfromgrandparent", builder, builder.getSearch("child"));
+ assertCorrectConfigFiles("inheritfromgrandparent");
+ }
+
+ @Test
+ public void testInheritance() throws IOException, ParseException {
+ try {
+ String dir = "src/test/derived/inheritance/";
+ SearchBuilder builder = new SearchBuilder();
+ builder.importFile(dir + "grandparent.sd");
+ builder.importFile(dir + "father.sd");
+ builder.importFile(dir + "mother.sd");
+ builder.importFile(dir + "child.sd");
+ builder.build();
+ } catch (IllegalArgumentException e) {
+ assertEquals(
+ "Inherited document 'datatype grandparent (code: -154107656)' already contains field 'overridden'. Can not override with 'overridden'.",
+ e.getMessage());
+ }
+ }
+
+ @Test
+ public void testIndexSettingInheritance() {
+ SDDocumentType parent = new SDDocumentType("parent");
+ Search parentSearch = new Search("parent", null);
+ parentSearch.addDocument(parent);
+ SDField prefixed = parent.addField("prefixed", DataType.STRING);
+ prefixed.parseIndexingScript("{ index }");
+ prefixed.addIndex(new Index("prefixed", true));
+
+ SDDocumentType child = new SDDocumentType("child");
+ child.inherit(parent);
+ Search childSearch = new Search("child", null);
+ childSearch.addDocument(child);
+
+ prefixed = (SDField)child.getField("prefixed");
+ assertNotNull(prefixed);
+ assertEquals(new Index("prefixed", true), childSearch.getIndex("prefixed"));
+ }
+
+ @Test
+ public void testFailTypesMismatch() throws IOException, ParseException {
+ String root = "src/test/derived/inheritancebadtypes/";
+ List<String> files = new LinkedList<>();
+ files.add(root + "parent.sd");
+ files.add(root + "child.sd");
+ File toDir = tmpDir.newFolder("to");
+ try {
+ Deriver.deriveDocuments(files, toDir.getPath());
+ fail("Import of child SD with type mismatch worked.");
+ } catch (RuntimeException e) {
+ assertTrue(e.getMessage().matches(".*already contains field 'a'.*"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/IntegerAttributeToStringIndexTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/IntegerAttributeToStringIndexTestCase.java
new file mode 100644
index 00000000000..b7371b66b29
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/IntegerAttributeToStringIndexTestCase.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:jon@zenior.no">Jon Bratseth</a>
+ */
+public class IntegerAttributeToStringIndexTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testIt() throws IOException, ParseException {
+ assertCorrectDeriving("integerattributetostringindex");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/LiteralBoostTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/LiteralBoostTestCase.java
new file mode 100644
index 00000000000..8d5c4d94939
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/LiteralBoostTestCase.java
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.processing.Processing;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static com.yahoo.searchdefinition.processing.AssertIndexingScript.assertIndexing;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bratseth
+ */
+public class LiteralBoostTestCase extends AbstractExportingTestCase {
+
+ /**
+ * Tests adding of literal boost constructs
+ */
+ @Test
+ public void testLiteralBoost() {
+ Search search=new Search("literalboost", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+ SDDocumentType document=new SDDocumentType("literalboost");
+ search.addDocument(document);
+ SDField field1= document.addField("a", DataType.STRING);
+ field1.parseIndexingScript("{ index }");
+ field1.setLiteralBoost(20);
+ RankProfile other=new RankProfile("other", search, rankProfileRegistry);
+ rankProfileRegistry.addRankProfile(other);
+ other.addRankSetting(new RankProfile.RankSetting("a", RankProfile.RankSetting.Type.LITERALBOOST, 333));
+
+ Processing.process(search, new BaseDeployLogger(), rankProfileRegistry, new QueryProfiles());
+ DerivedConfiguration derived=new DerivedConfiguration(search, rankProfileRegistry);
+
+ // Check attribute fields
+ derived.getAttributeFields(); // TODO: assert content
+
+ // Check il script addition
+ assertIndexing(Arrays.asList("clear_state | guard { input a | tokenize normalize stem:\"SHORTEST\" | index a; }",
+ "clear_state | guard { input a | tokenize | index a_literal; }"),
+ search);
+
+ // Check index info addition
+ IndexInfo indexInfo=derived.getIndexInfo();
+ assertTrue(indexInfo.hasCommand("a","literal-boost"));
+ }
+
+ /**
+ * Tests adding a literal boost in a non-default rank profile only
+ */
+ @Test
+ public void testNonDefaultRankLiteralBoost() {
+ Search search=new Search("literalboost", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+ SDDocumentType document=new SDDocumentType("literalboost");
+ search.addDocument(document);
+ SDField field1= document.addField("a", DataType.STRING);
+ field1.parseIndexingScript("{ index }");
+ RankProfile other=new RankProfile("other", search, rankProfileRegistry);
+ rankProfileRegistry.addRankProfile(other);
+ other.addRankSetting(new RankProfile.RankSetting("a", RankProfile.RankSetting.Type.LITERALBOOST, 333));
+
+ search = SearchBuilder.buildFromRawSearch(search, rankProfileRegistry);
+ DerivedConfiguration derived = new DerivedConfiguration(search, rankProfileRegistry);
+
+ // Check il script addition
+ assertIndexing(Arrays.asList("clear_state | guard { input a | tokenize normalize stem:\"SHORTEST\" | index a; }",
+ "clear_state | guard { input a | tokenize | index a_literal; }"),
+ search);
+
+ // Check index info addition
+ IndexInfo indexInfo=derived.getIndexInfo();
+ assertTrue(indexInfo.hasCommand("a","literal-boost"));
+ }
+
+ /** Tests literal boosts in two fields going to the same index */
+ @Test
+ public void testTwoLiteralBoostFields() {
+ Search search=new Search("msb", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+ SDDocumentType document=new SDDocumentType("msb");
+ search.addDocument(document);
+ SDField field1= document.addField("title", DataType.STRING);
+ field1.parseIndexingScript("{ summary | index }");
+ field1.setLiteralBoost(20);
+ SDField field2= document.addField("body", DataType.STRING);
+ field2.parseIndexingScript("{ summary | index }");
+ field2.setLiteralBoost(20);
+
+ search = SearchBuilder.buildFromRawSearch(search, rankProfileRegistry);
+ new DerivedConfiguration(search, rankProfileRegistry);
+ assertIndexing(Arrays.asList("clear_state | guard { input title | tokenize normalize stem:\"SHORTEST\" | summary title | index title; }",
+ "clear_state | guard { input body | tokenize normalize stem:\"SHORTEST\" | summary body | index body; }",
+ "clear_state | guard { input title | tokenize | index title_literal; }",
+ "clear_state | guard { input body | tokenize | index body_literal; }"),
+ search);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/MailTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/MailTestCase.java
new file mode 100644
index 00000000000..53d29bd3ca1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/MailTestCase.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.UnprocessingSearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests VDS+streaming configuration deriving
+ *
+ * @author bratseth
+ */
+public class MailTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testMail() throws IOException, ParseException {
+ String dir = "src/test/derived/mail/";
+ SearchBuilder sb = new SearchBuilder();
+ sb.importFile(dir + "mail.sd");
+ assertCorrectDeriving(sb, dir);
+ }
+
+ @Test
+ public void testMailDocumentsonlyDeriving() {
+ String root = "src/test/derived/mail/";
+ File toDir = new File("temp/documentderiver/");
+ if (!toDir.exists()) {
+ toDir.mkdir();
+ }
+ List<String> files = new ArrayList<>();
+ files.add(root + "mail.sd");
+ Deriver.deriveDocuments(files, toDir.getPath());
+ try {
+ assertEqualFiles(root + "onlydoc/documentmanager.cfg",
+ toDir.getPath() + "/documentmanager.cfg");
+ } catch (IOException e) {
+ throw new RuntimeException("Exception while comparing files", e);
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/MultipleSummariesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/MultipleSummariesTestCase.java
new file mode 100644
index 00000000000..8538944c13c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/MultipleSummariesTestCase.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving a configuration with multiple summaries
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class MultipleSummariesTestCase extends AbstractExportingTestCase {
+ @Test
+ @Ignore
+ public void testMultipleSummaries() throws IOException, ParseException {
+ assertCorrectDeriving("multiplesummaries");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionsTestCase.java
new file mode 100644
index 00000000000..2dc96901275
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/NativeRankTypeDefinitionsTestCase.java
@@ -0,0 +1,91 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.RankType;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+/**
+ * Testing stuff related to native rank type definitions
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class NativeRankTypeDefinitionsTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testTables() {
+ assertEquals(NativeTable.Type.FIRST_OCCURRENCE.getName(), "firstOccurrenceTable");
+ assertEquals(NativeTable.Type.OCCURRENCE_COUNT.getName(), "occurrenceCountTable");
+ assertEquals(NativeTable.Type.PROXIMITY.getName(), "proximityTable");
+ assertEquals(NativeTable.Type.REVERSE_PROXIMITY.getName(), "reverseProximityTable");
+ assertEquals(NativeTable.Type.WEIGHT.getName(), "weightTable");
+ }
+ @Test
+ public void testDefinitions() {
+ NativeRankTypeDefinitionSet defs = new NativeRankTypeDefinitionSet("default");
+
+ NativeRankTypeDefinition rank;
+ Iterator<NativeTable> tables;
+
+ assertEquals(4, defs.types().size());
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.EMPTY);
+ assertNotNull(rank);
+ assertEquals(RankType.EMPTY, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "linear(0,0)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "linear(0,0)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.ABOUT);
+ assertNotNull(rank);
+ assertEquals(RankType.ABOUT, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(8000,12.50)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(500,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(400,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "linear(1,0)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.IDENTITY);
+ assertNotNull(rank);
+ assertEquals(RankType.IDENTITY, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(100,12.50)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(5000,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(3000,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "linear(1,0)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ rank = defs.getRankTypeDefinition(RankType.TAGS);
+ assertNotNull(rank);
+ assertEquals(RankType.TAGS, rank.getType());
+ tables = rank.rankSettingIterator();
+ assertEquals(new NativeTable(NativeTable.Type.FIRST_OCCURRENCE, "expdecay(8000,12.50)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.OCCURRENCE_COUNT, "loggrowth(1500,4000,19)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.PROXIMITY, "expdecay(500,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.REVERSE_PROXIMITY, "expdecay(400,3)"), tables.next());
+ assertEquals(new NativeTable(NativeTable.Type.WEIGHT, "loggrowth(38,50,1)"), tables.next());
+ assertFalse(tables.hasNext());
+ }
+
+ {
+ assertEquals(RankType.ABOUT, defs.getRankTypeDefinition(RankType.DEFAULT).getType());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/OrderIlscriptsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/OrderIlscriptsTestCase.java
new file mode 100755
index 00000000000..0d72b90a41c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/OrderIlscriptsTestCase.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class OrderIlscriptsTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testOrderIlscripts() throws IOException, ParseException {
+ assertCorrectDeriving("orderilscripts");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/PrefixExactAttributeTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/PrefixExactAttributeTestCase.java
new file mode 100644
index 00000000000..77cea993131
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/PrefixExactAttributeTestCase.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving of various field types
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class PrefixExactAttributeTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testTypes() throws IOException, ParseException {
+ assertCorrectDeriving("prefixexactattribute");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/RankProfilesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/RankProfilesTestCase.java
new file mode 100644
index 00000000000..222871c78b7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/RankProfilesTestCase.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests a search definition with various rank profiles having different settings
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class RankProfilesTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testRankProfiles() throws IOException, ParseException {
+ assertCorrectDeriving("rankprofiles");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/RankPropertiesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/RankPropertiesTestCase.java
new file mode 100644
index 00000000000..211e622e7a6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/RankPropertiesTestCase.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:jon@zenior.no">Jon Bratseth</a>
+ */
+public class RankPropertiesTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testRankProperties() throws IOException, ParseException {
+ assertCorrectDeriving("rankproperties");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/SearchOrdererTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SearchOrdererTestCase.java
new file mode 100644
index 00000000000..084366ddcbb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SearchOrdererTestCase.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import org.junit.Test;
+
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class SearchOrdererTestCase extends SearchDefinitionTestCase {
+
+ private Map<String, Search> createSearchDefinitions() {
+ Map<String, Search> searchDefinitions = new HashMap<>();
+
+ Search grandParent = createSearchDefinition("grandParent", searchDefinitions);
+
+ Search mother = createSearchDefinition("mother", searchDefinitions);
+ inherit(mother, grandParent);
+
+ Search father = createSearchDefinition("father", searchDefinitions);
+ inherit(father, grandParent);
+
+ Search daugther = createSearchDefinition("daughter", searchDefinitions);
+ inherit(daugther, father);
+ inherit(daugther, mother);
+
+ Search son = createSearchDefinition("son", searchDefinitions);
+ inherit(son, father);
+ inherit(son, mother);
+
+ Search product = createSearchDefinition("product", searchDefinitions);
+
+ Search pc = createSearchDefinition("pc", searchDefinitions);
+ inherit(pc, product);
+
+ createSearchDefinition("alone", searchDefinitions);
+
+ return searchDefinitions;
+ }
+
+ private Search createSearchDefinition(String name, Map<String, Search> searchDefinitions) {
+ Search search = new Search(name, null);
+ SDDocumentType document = new SDDocumentType(name);
+ search.addDocument(document);
+ searchDefinitions.put(search.getName(), search);
+ return search;
+ }
+
+ private void inherit(Search inheritee, Search inherited) {
+ inheritee.getDocument().inherit(inherited.getDocument());
+ }
+
+ private void assertOrder(List<String> rightOrder, List<String> input) {
+ Map<String, Search> searchDefinitions = createSearchDefinitions();
+ SearchOrderer orderer = new SearchOrderer();
+ List<Search> unordered = new ArrayList<>();
+ for (String anInput : input) {
+ Search search = searchDefinitions.get(anInput);
+ assertNotNull(anInput + " exists", search);
+ unordered.add(search);
+ }
+ List<Search> ordered = orderer.order(unordered);
+ List<String> names = new LinkedList<>();
+ for (int i = 0; i < rightOrder.size(); i++) {
+ Search search = ordered.get(i);
+ names.add(search.getName());
+ }
+ assertEquals(rightOrder.toString(), names.toString());
+ }
+
+ @Test
+ public void testPerfectOrderingIsKept() {
+ assertOrder(Arrays.asList("alone", "grandParent", "father", "mother", "daughter", "product", "pc", "son"),
+ Arrays.asList("grandParent", "mother", "father", "daughter", "son", "product", "pc", "alone"));
+ }
+ @Test
+ public void testOneLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "father", "mother", "daughter", "product", "pc", "son"),
+ Arrays.asList("grandParent", "daughter", "son", "mother", "father", "pc", "product", "alone"));
+ }
+ @Test
+ public void testMultiLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "father", "mother", "daughter", "product", "pc", "son"),
+ Arrays.asList("daughter", "son", "mother", "father", "grandParent", "pc", "product", "alone"));
+ }
+ @Test
+ public void testAloneIsKeptInPlaceWithMultiLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "father", "mother", "daughter", "product", "pc", "son"),
+ Arrays.asList("alone", "daughter", "son", "mother", "father", "grandParent", "pc", "product"));
+ }
+ @Test
+ public void testPartialMultiLevelReordering() {
+ assertOrder(Arrays.asList("alone", "grandParent", "father", "mother", "daughter", "product", "pc", "son"),
+ Arrays.asList("daughter", "grandParent", "mother", "son", "father", "product", "pc", "alone"));
+ }
+ @Test
+ public void testMultilevelReorderingAccrossHierarchies() {
+ assertOrder(Arrays.asList("alone", "grandParent", "father", "mother", "daughter", "product", "pc", "son"),
+ Arrays.asList("daughter", "pc", "son", "mother", "grandParent", "father", "product", "alone"));
+ }
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/SimpleInheritTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SimpleInheritTestCase.java
new file mode 100644
index 00000000000..e32a80a4eb5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SimpleInheritTestCase.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Tests really simple inheriting
+ */
+public class SimpleInheritTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testEmptyChild() throws IOException, ParseException {
+ String name = "emptychild";
+ final String expectedResultsDirName = "src/test/derived/" + name + "/";
+
+ SearchBuilder builder = new SearchBuilder();
+ builder.importFile(expectedResultsDirName + "parent.sd");
+ builder.importFile(expectedResultsDirName + "child.sd");
+ builder.build();
+
+ Search search = builder.getSearch("child");
+
+ String toDirName = "temp/" + name;
+ File toDir = new File(toDirName);
+ toDir.mkdirs();
+ deleteContent(toDir);
+
+ DerivedConfiguration config = new DerivedConfiguration(search, builder.getRankProfileRegistry());
+ config.export(toDirName);
+
+ checkDir(toDirName, expectedResultsDirName);
+ }
+
+ private void checkDir(String toDirName, String expectedResultsDirName) throws IOException {
+ File[] files = new File(expectedResultsDirName).listFiles();
+ for (File file : files) {
+ if (!file.getName().endsWith(".cfg")) {
+ continue;
+ }
+ assertEqualFiles(file.getPath(), toDirName + "/" + file.getName());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/SortingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SortingTestCase.java
new file mode 100644
index 00000000000..15aa8e63220
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SortingTestCase.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests sort settings
+ *
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class SortingTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testDocumentDeriving() throws IOException, ParseException {
+ assertCorrectDeriving("sorting");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/StreamingStructTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/StreamingStructTestCase.java
new file mode 100755
index 00000000000..ee4ad4fcd8a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/StreamingStructTestCase.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Tests VSM configuration deriving for structs
+ *
+ * @author <a href="bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class StreamingStructTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testStreamingStruct() throws IOException, ParseException {
+ assertCorrectDeriving("streamingstruct");
+ }
+
+ @Test
+ public void testStreamingStructExplicitDefaultSummaryClass() throws IOException, ParseException {
+ // Tests an issue for mail in Vespa 4.1; specific overrides of default summary class
+ assertCorrectDeriving("streamingstructdefault");
+ }
+
+ @Test
+ public void testStreamingStructDocumentsonlyDeriving() throws IOException {
+ String root = "src/test/derived/streamingstruct/";
+ String temp = "temp/documentderiver/";
+ new File(temp).mkdir();
+ Deriver.deriveDocuments(Arrays.asList(root + "streamingstruct.sd"), temp);
+ assertEqualFiles(root + "/onlydoc/documentmanager.cfg",
+ temp + "/documentmanager.cfg");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/StructAnyOrderTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/StructAnyOrderTestCase.java
new file mode 100755
index 00000000000..1f0ca253dc0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/StructAnyOrderTestCase.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class StructAnyOrderTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testStructAnyOrder() throws IOException, ParseException {
+ assertCorrectDeriving("structanyorder");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryMapTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryMapTestCase.java
new file mode 100644
index 00000000000..58316111c55
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryMapTestCase.java
@@ -0,0 +1,152 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.*;
+import com.yahoo.vespa.config.search.SummarymapConfig;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.searchdefinition.processing.Processing;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+/**
+ * Tests summary map extraction
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class SummaryMapTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/simple.sd");
+ SummaryMap summaryMap=new SummaryMap(search, new Summaries(search, new BaseDeployLogger()));
+
+ Iterator transforms=summaryMap.resultTransformIterator();
+ FieldResultTransform transform = (FieldResultTransform)transforms.next();
+ assertEquals("dyndesc", transform.getFieldName());
+ assertEquals(SummaryTransform.DYNAMICTEASER,transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("dynlong", transform.getFieldName());
+ assertEquals(SummaryTransform.DYNAMICTEASER,transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("dyndesc2", transform.getFieldName());
+ assertEquals(SummaryTransform.DYNAMICTEASER,transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("measurement", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE,transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("rankfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.RANKFEATURES, transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("summaryfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.SUMMARYFEATURES, transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("popsiness", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE,transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("popularity", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE,transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("access", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE,transform.getTransform());
+
+ assertTrue(!transforms.hasNext());
+ }
+ @Test
+ public void testPositionDeriving() throws IOException, ParseException {
+ Search search = new Search("store", null);
+ SDDocumentType document = new SDDocumentType("store");
+ search.addDocument(document);
+ String fieldName = "location";
+ SDField field = document.addField(fieldName, PositionDataType.INSTANCE);
+ field.parseIndexingScript("{ attribute | summary }");
+ Processing.process(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles());
+ SummaryMap summaryMap = new SummaryMap(search, new Summaries(search, new BaseDeployLogger()));
+
+ Iterator transforms = summaryMap.resultTransformIterator();
+
+ FieldResultTransform transform = (FieldResultTransform)transforms.next();
+
+ assertEquals(fieldName, transform.getFieldName());
+ assertEquals(SummaryTransform.GEOPOS, transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals(PositionDataType.getPositionSummaryFieldName(fieldName), transform.getFieldName());
+ assertEquals(SummaryTransform.POSITIONS, transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals(PositionDataType.getDistanceSummaryFieldName(fieldName), transform.getFieldName());
+ assertEquals(SummaryTransform.DISTANCE,transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("rankfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.RANKFEATURES, transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("summaryfeatures", transform.getFieldName());
+ assertEquals(SummaryTransform.SUMMARYFEATURES, transform.getTransform());
+
+ transform = (FieldResultTransform)transforms.next();
+ assertEquals("location_zcurve", transform.getFieldName());
+ assertEquals(SummaryTransform.ATTRIBUTE,transform.getTransform());
+
+ assertTrue(!transforms.hasNext());
+
+ SummarymapConfig.Builder scb = new SummarymapConfig.Builder();
+ summaryMap.getConfig(scb);
+ SummarymapConfig c = new SummarymapConfig(scb);
+
+ assertEquals(-1, c.defaultoutputclass());
+ assertEquals(c.override().size(), 6);
+
+ assertEquals(c.override(0).field(), fieldName);
+ assertEquals(c.override(0).command(), "geopos");
+ assertEquals(c.override(0).arguments(), PositionDataType.getZCurveFieldName(fieldName));
+
+ assertEquals(c.override(1).field(), PositionDataType.getPositionSummaryFieldName(fieldName));
+ assertEquals(c.override(1).command(), "positions");
+ assertEquals(c.override(1).arguments(), PositionDataType.getZCurveFieldName(fieldName));
+
+ assertEquals(c.override(2).field(), PositionDataType.getDistanceSummaryFieldName(fieldName));
+ assertEquals(c.override(2).command(), "absdist");
+ assertEquals(c.override(2).arguments(), PositionDataType.getZCurveFieldName(fieldName));
+
+ assertEquals(c.override(3).field(), "rankfeatures");
+ assertEquals(c.override(3).command(), "rankfeatures");
+ assertEquals(c.override(3).arguments(), "");
+
+ assertEquals(c.override(4).field(), "summaryfeatures");
+ assertEquals(c.override(4).command(), "summaryfeatures");
+ assertEquals(c.override(4).arguments(), "");
+
+ assertEquals(c.override(5).field(), "location_zcurve");
+ assertEquals(c.override(5).command(), "attribute");
+ assertEquals(c.override(5).arguments(), "location_zcurve");
+ }
+
+ @Test
+ public void testFailOnSummaryFieldSourceCollision() throws IOException, ParseException {
+ try {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/summaryfieldcollision.sd");
+ } catch (Exception e) {
+ assertTrue(e.getMessage().matches(".*equally named field.*"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryTestCase.java
new file mode 100644
index 00000000000..ceac1186b6e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/SummaryTestCase.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+/**
+ * Tests summary extraction
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon S Bratseth</a>
+ */
+public class SummaryTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testDeriving() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/simple.sd");
+ SummaryClass summary=new SummaryClass(search,search.getSummary("default"), new BaseDeployLogger());
+ assertEquals("default",summary.getName());
+
+ Iterator fields=summary.fieldIterator();
+
+ SummaryClassField field;
+
+ assertEquals(13, summary.getFieldCount());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("exactemento",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("exact",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("title",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("description",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("dyndesc",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("longdesc",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("longstat",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("dynlong",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("dyndesc2",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("measurement",field.getName());
+ assertEquals(SummaryClassField.Type.INTEGER,field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("rankfeatures",field.getName());
+ assertEquals(SummaryClassField.Type.FEATUREDATA, field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("summaryfeatures",field.getName());
+ assertEquals(SummaryClassField.Type.FEATUREDATA, field.getType());
+
+ field=(SummaryClassField)fields.next();
+ assertEquals("documentid",field.getName());
+ assertEquals(SummaryClassField.Type.LONGSTRING,field.getType());
+ }
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/TwoStreamingStructsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/TwoStreamingStructsTestCase.java
new file mode 100644
index 00000000000..0db377f43ea
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/TwoStreamingStructsTestCase.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Test structs for streaming with another unrelated .sd present
+ *
+ * @author arnej27959
+ */
+public class TwoStreamingStructsTestCase extends AbstractExportingTestCase {
+ @Test
+ public void testTwoStreamingStructsExporting() throws ParseException, IOException {
+
+ String root = "src/test/derived/twostreamingstructs";
+ SearchBuilder builder = new SearchBuilder();
+ builder.importFile(root + "/streamingstruct.sd");
+ builder.importFile(root + "/whatever.sd");
+ builder.build();
+ assertCorrectDeriving(builder, builder.getSearch("streamingstruct"), root);
+
+ builder = new SearchBuilder();
+ builder.importFile(root + "/streamingstruct.sd");
+ builder.importFile(root + "/whatever.sd");
+ builder.build();
+ assertCorrectDeriving(builder, builder.getSearch("streamingstruct"), root);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/TypeConversionTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/TypeConversionTestCase.java
new file mode 100644
index 00000000000..315f278f942
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/TypeConversionTestCase.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.processing.Processing;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+/**
+ * Tests automatic type conversion using multifield indices
+ *
+ * @author bratseth
+ */
+public class TypeConversionTestCase extends SearchDefinitionTestCase {
+
+ /**
+ * Tests that exact-string stuff is not spilled over to the default index
+ */
+ @Test
+ public void testExactStringToStringTypeConversion() {
+ Search search = new Search("test", null);
+ RankProfileRegistry rankProfileRegistry = RankProfileRegistry.createRankProfileRegistryWithBuiltinRankProfiles(search);
+ SDDocumentType document = new SDDocumentType("test");
+ search.addDocument(document);
+ SDField a = new SDField("a", DataType.STRING);
+ a.parseIndexingScript("{ index }");
+ document.addField(a);
+
+ Processing.process(search, new BaseDeployLogger(), rankProfileRegistry, new QueryProfiles());
+ DerivedConfiguration derived = new DerivedConfiguration(search, rankProfileRegistry);
+ IndexInfo indexInfo = derived.getIndexInfo();
+ assertFalse(indexInfo.hasCommand("default", "compact-to-term"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/derived/TypesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/derived/TypesTestCase.java
new file mode 100644
index 00000000000..7908bb3a9f3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/derived/TypesTestCase.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.derived;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests deriving of various field types
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class TypesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testTypes() throws IOException, ParseException {
+ assertCorrectDeriving("types");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertIndexingScript.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertIndexingScript.java
new file mode 100644
index 00000000000..bcf5901dec2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertIndexingScript.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.derived.IndexingScript;
+import com.yahoo.vespa.indexinglanguage.expressions.Expression;
+import com.yahoo.vespa.indexinglanguage.parser.ParseException;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class AssertIndexingScript {
+
+ public static void assertIndexing(List<String> expected, Search search) {
+ assertIndexing(expected, new IndexingScript(search).expressions());
+ }
+
+ public static void assertIndexing(List<String> expected, IndexingScript script) {
+ assertIndexing(expected, script.expressions());
+ }
+
+ public static void assertIndexing(List<String> expected, Iterable<Expression> actual) {
+ List<String> parsedExpected = new LinkedList<>();
+ for (String str : expected) {
+ try {
+ parsedExpected.add(Expression.fromString(str).toString());
+ } catch (ParseException e) {
+ fail(e.getMessage());
+ }
+ }
+ for (Expression actualExp : actual) {
+ String str = actualExp.toString();
+ assertTrue("Unexpected: " + str, parsedExpected.remove(str));
+ }
+ assertTrue("Missing: " + parsedExpected.toString(), parsedExpected.isEmpty());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertSearchBuilder.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertSearchBuilder.java
new file mode 100644
index 00000000000..e8d6a416eec
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AssertSearchBuilder.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class AssertSearchBuilder {
+
+ public static void assertBuilds(String searchDefinitionFileName) throws IOException, ParseException {
+ assertNotNull(SearchBuilder.buildFromFile(searchDefinitionFileName));
+ }
+
+ public static void assertBuildFails(String searchDefinitionFileName, String expectedException)
+ throws IOException, ParseException {
+ try {
+ SearchBuilder.buildFromFile(searchDefinitionFileName);
+ fail(searchDefinitionFileName);
+ } catch (IllegalArgumentException e) {
+ assertEquals(expectedException, e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributeIndexTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributeIndexTestCase.java
new file mode 100644
index 00000000000..26d22908060
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributeIndexTestCase.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.UnprocessingSearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+/**
+ * Test AttributeIndex processor.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class AttributeIndexTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testAttributeIndex() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/attributeindex.sd");
+
+ assertTrue(search.getField("nosettings").getAttributes().get("nosettings") != null);
+
+ assertTrue(search.getField("specifyname").getAttributes().get("newname") != null);
+
+ assertTrue(search.getField("specifyname2").getAttributes().get("newname2") != null);
+
+ assertTrue(search.getField("withstaticrankname").getAttributes().get("withstaticrankname") != null);
+
+ assertTrue(search.getField("withstaticrankname").getAttributes().get("someothername") != null);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributePropertiesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributePropertiesTestCase.java
new file mode 100644
index 00000000000..1fa275bc505
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributePropertiesTestCase.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.UnprocessingSearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.fail;
+/**
+ * Test AttributeProperties processor.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class AttributePropertiesTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testInvalidAttributeProperties() throws IOException, ParseException {
+ try {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/attributeproperties1.sd");
+ new AttributeProperties(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process();
+ fail("attribute property should not be set");
+ } catch (RuntimeException e) {
+ // empty
+ }
+ }
+ @Test
+ public void testValidAttributeProperties() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/attributeproperties2.sd");
+ new AttributeProperties(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributesExactMatchTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributesExactMatchTestCase.java
new file mode 100644
index 00000000000..2081ac6accc
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/AttributesExactMatchTestCase.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+/**
+ * Attributes should be implicitly exact-match in some cases
+ * @author vegardh
+ *
+ */
+public class AttributesExactMatchTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testAttributesExactMatch() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/attributesexactmatch.sd");
+ assertEquals(search.getField("color").getMatching().getType(), Matching.Type.EXACT);
+ assertEquals(search.getField("artist").getMatching().getType(), Matching.Type.WORD);
+ assertEquals(search.getField("drummer").getMatching().getType(), Matching.Type.WORD);
+ assertEquals(search.getField("guitarist").getMatching().getType(), Matching.Type.TEXT);
+ assertEquals(search.getField("saxophonist_arr").getMatching().getType(), Matching.Type.WORD);
+ assertEquals(search.getField("flutist").getMatching().getType(), Matching.Type.TEXT);
+
+ assertFalse(search.getField("genre").getMatching().getType().equals(Matching.Type.EXACT));
+ assertFalse(search.getField("title").getMatching().getType().equals(Matching.Type.EXACT));
+ assertFalse(search.getField("trumpetist").getMatching().getType().equals(Matching.Type.EXACT));
+ assertFalse(search.getField("genre").getMatching().getType().equals(Matching.Type.WORD));
+ assertFalse(search.getField("title").getMatching().getType().equals(Matching.Type.WORD));
+ assertFalse(search.getField("trumpetist").getMatching().getType().equals(Matching.Type.WORD));
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/BoldingTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/BoldingTestCase.java
new file mode 100644
index 00000000000..a61e59e5d7f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/BoldingTestCase.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.UnprocessingSearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+/**
+ * @author <a href="mailto:mathiasm@yahoo-inc.com">Mathias M\u00F8lster Lidal</a>
+ */
+public class BoldingTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testBoldingNonString() throws IOException, ParseException {
+ try {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/processing/boldnonstring.sd");
+ new Bolding(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process();
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("'bolding: on' for non-text field"));
+ }
+ }
+}
+
+
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSearchFieldsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSearchFieldsTestCase.java
new file mode 100644
index 00000000000..03a014b6ca6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSearchFieldsTestCase.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.searchdefinition.derived.Deriver;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+import java.io.File;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ImplicitSearchFieldsTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testRequireThatExtraFieldsAreIncluded() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/nextgen/extrafield.sd");
+ assertNotNull(search);
+
+ SDDocumentType docType = search.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("rankfeatures"));
+ assertNotNull(docType.getField("summaryfeatures"));
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertEquals(4, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatSummaryFieldsAreIncluded() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/nextgen/summaryfield.sd");
+ assertNotNull(search);
+
+ SDDocumentType docType = search.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("rankfeatures"));
+ assertNotNull(docType.getField("summaryfeatures"));
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertNotNull(docType.getField("cox"));
+ assertEquals(5, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatBoldedSummaryFieldsAreIncluded() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/nextgen/boldedsummaryfields.sd");
+ assertNotNull(search);
+
+ SDDocumentType docType = search.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("rankfeatures"));
+ assertNotNull(docType.getField("summaryfeatures"));
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertNotNull(docType.getField("baz"));
+ assertNotNull(docType.getField("cox"));
+ assertEquals(6, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatUntransformedSummaryFieldsAreIgnored() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/nextgen/untransformedsummaryfields.sd");
+ assertNotNull(search);
+
+ SDDocumentType docType = search.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("rankfeatures"));
+ assertNotNull(docType.getField("summaryfeatures"));
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertNotNull(docType.getField("baz"));
+ assertEquals(5, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatDynamicSummaryFieldsAreIgnored() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/nextgen/dynamicsummaryfields.sd");
+ assertNotNull(search);
+
+ SDDocumentType docType = search.getDocument();
+ assertNotNull(docType);
+ assertNotNull(docType.getField("rankfeatures"));
+ assertNotNull(docType.getField("summaryfeatures"));
+ assertNotNull(docType.getField("foo"));
+ assertNotNull(docType.getField("bar"));
+ assertEquals(4, docType.getFieldCount());
+ }
+
+ @Test
+ public void testRequireThatDerivedConfigurationWorks() throws IOException, ParseException {
+ SearchBuilder sb = new SearchBuilder();
+ sb.importFile("src/test/examples/nextgen/simple.sd");
+ sb.build();
+ assertNotNull(sb.getSearch());
+ new DerivedConfiguration(sb.getSearch(), sb.getRankProfileRegistry());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitStructTypesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitStructTypesTestCase.java
new file mode 100644
index 00000000000..371b0bd565f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitStructTypesTestCase.java
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.document.*;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+public class ImplicitStructTypesTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testRequireThatImplicitStructsAreCreated() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/nextgen/toggleon.sd");
+ assertNotNull(search);
+
+ SDDocumentType docType = search.getDocument();
+ assertNotNull(docType);
+ assertStruct(docType, PositionDataType.INSTANCE);
+ }
+ @Test
+ public void testRequireThatImplicitStructsAreUsed() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/nextgen/implicitstructtypes.sd");
+ assertNotNull(search);
+
+ SDDocumentType docType = search.getDocument();
+ assertNotNull(docType);
+
+ assertField(docType, "doc_str", DataType.STRING);
+ assertField(docType, "doc_str_sum", DataType.STRING);
+ assertField(docType, "doc_uri", DataType.URI);
+ assertField(docType, "docsum_str", DataType.STRING);
+ assertField(docType, "rankfeatures", DataType.STRING);
+ assertField(docType, "summaryfeatures", DataType.STRING);
+ }
+
+ @SuppressWarnings({ "UnusedDeclaration" })
+ private static void assertStruct(SDDocumentType docType, StructDataType expectedStruct) {
+ // TODO: When structs are refactored from a static register to a member of the owning document types, this test
+ // TODO: must be changed to retrieve struct type from the provided document type.
+ StructDataType structType = (StructDataType) docType.getType(expectedStruct.getName()).getStruct();
+ assertNotNull(structType);
+ for (Field expectedField : expectedStruct.getFields()) {
+ Field field = structType.getField(expectedField.getName());
+ assertNotNull(field);
+ assertEquals(expectedField.getDataType(), field.getDataType());
+ }
+ assertEquals(expectedStruct.getFieldCount(), structType.getFieldCount());
+ }
+
+ private static void assertField(SDDocumentType docType, String fieldName, DataType type) {
+ Field field = getSecretField(docType, fieldName); // TODO: get rid of this stupidity
+ assertNotNull(field);
+ assertEquals(type, field.getDataType());
+ assertTrue(field instanceof SDField);
+ }
+
+ private static Field getSecretField(SDDocumentType docType, String fieldName) {
+ for (Field field : docType.fieldSet()) {
+ if (field.getName().equals(fieldName)) {
+ return field;
+ }
+ }
+ return null;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummariesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummariesTestCase.java
new file mode 100644
index 00000000000..09f10961074
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummariesTestCase.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ImplicitSummariesTestCase {
+
+ @Test
+ public void requireThatSummaryFromAttributeDoesNotWarn() throws IOException, ParseException {
+ LogHandler log = new LogHandler();
+ Logger.getLogger("").addHandler(log);
+
+ Search search = SearchBuilder.buildFromFile("src/test/examples/implicitsummaries_attribute.sd");
+ assertNotNull(search);
+ assertTrue(log.records.isEmpty());
+ }
+
+ private static class LogHandler extends Handler {
+
+ final List<LogRecord> records = new ArrayList<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ if (record.getLevel() == Level.WARNING ||
+ record.getLevel() == Level.SEVERE)
+ {
+ records.add(record);
+ }
+ }
+
+ @Override
+ public void flush() {
+
+ }
+
+ @Override
+ public void close() throws SecurityException {
+
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFieldsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFieldsTestCase.java
new file mode 100644
index 00000000000..15a47d4a4af
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ImplicitSummaryFieldsTestCase.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.documentmodel.DocumentSummary;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ImplicitSummaryFieldsTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testRequireThatImplicitFieldsAreCreated() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/implicitsummaryfields.sd");
+ assertNotNull(search);
+
+ DocumentSummary docsum = search.getSummary("default");
+ assertNotNull(docsum);
+ assertNotNull(docsum.getSummaryField("rankfeatures"));
+ assertNotNull(docsum.getSummaryField("summaryfeatures"));
+ assertEquals(2, docsum.getSummaryFields().size());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingInputsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingInputsTestCase.java
new file mode 100644
index 00000000000..48a92f40ed6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingInputsTestCase.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.searchdefinition.processing.AssertSearchBuilder.assertBuildFails;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class IndexingInputsTestCase {
+
+ @Test
+ public void requireThatExtraFieldInputExtraFieldThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_extra_field.sd",
+ "For search 'indexing_extra_field_input_extra_field', field 'bar': Indexing script refers " +
+ "to field 'bar' which does not exist in document type " +
+ "'indexing_extra_field_input_extra_field'.");
+ }
+
+ @Test
+ public void requireThatExtraFieldInputImplicitThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_implicit.sd",
+ "For search 'indexing_extra_field_input_implicit', field 'foo': Indexing script refers to " +
+ "field 'foo' which does not exist in document type 'indexing_extra_field_input_implicit'.");
+ }
+
+ @Test
+ public void requireThatExtraFieldInputNullThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_null.sd",
+ "For search 'indexing_extra_field_input_null', field 'foo': Indexing script refers to field " +
+ "'foo' which does not exist in document type 'indexing_extra_field_input_null'.");
+ }
+
+ @Test
+ public void requireThatExtraFieldInputSelfThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_extra_field_input_self.sd",
+ "For search 'indexing_extra_field_input_self', field 'foo': Indexing script refers to field " +
+ "'foo' which does not exist in document type 'indexing_extra_field_input_self'.");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingOutputsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingOutputsTestCase.java
new file mode 100644
index 00000000000..71d995ece36
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingOutputsTestCase.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.searchdefinition.processing.AssertSearchBuilder.assertBuildFails;
+
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class IndexingOutputsTestCase {
+
+ @Test
+ public void requireThatOutputOtherFieldThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_output_other_field.sd",
+ "For search 'indexing_output_other_field', field 'foo': Indexing expression 'index bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void requireThatOutputConflictThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_output_conflict.sd",
+ "For search 'indexing_output_confict', field 'bar': For expression 'index bar': Attempting " +
+ "to assign conflicting values to field 'bar'.");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingScriptRewriterTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingScriptRewriterTestCase.java
new file mode 100644
index 00000000000..5a1b71db9d7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingScriptRewriterTestCase.java
@@ -0,0 +1,196 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.document.DataType;
+import com.yahoo.searchdefinition.Index;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.BooleanIndexDefinition;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+
+import static com.yahoo.searchdefinition.processing.AssertIndexingScript.assertIndexing;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class IndexingScriptRewriterTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testSetLanguageRewriting() {
+ assertIndexingScript("{ input test | set_language; }",
+ createField("test", DataType.STRING, "{ set_language }"));
+ }
+
+ @Test
+ public void testSummaryRewriting() {
+ assertIndexingScript("{ input test | summary test; }",
+ createField("test", DataType.STRING, "{ summary }"));
+ }
+
+ @Test
+ public void testDynamicSummaryRewriting() {
+ SDField field = createField("test", DataType.STRING, "{ summary }");
+ field.addSummaryField(createDynamicSummaryField(field, "dyn"));
+ assertIndexingScript("{ input test | tokenize normalize stem:\"SHORTEST\" | summary dyn; }", field);
+ }
+
+ @Test
+ public void testSummaryRewritingWithIndexing() {
+ assertIndexingScript("{ input test | tokenize normalize stem:\"SHORTEST\" | summary test | index test; }",
+ createField("test", DataType.STRING, "{ summary | index }"));
+ }
+
+ @Test
+ public void testDynamicAndStaticSummariesRewritingWithIndexing() {
+ SDField field = createField("test", DataType.STRING, "{ summary | index }");
+ field.addSummaryField(createDynamicSummaryField(field, "dyn"));
+ field.addSummaryField(createStaticSummaryField(field, "test"));
+ field.addSummaryField(createStaticSummaryField(field, "other"));
+ field.addSummaryField(createDynamicSummaryField(field, "dyn2"));
+ assertIndexingScript("{ input test | tokenize normalize stem:\"SHORTEST\" | summary dyn | summary dyn2 | summary other | " +
+ "summary test | index test; }", field);
+ }
+
+ @Test
+ public void testIntSummaryRewriting() {
+ assertIndexingScript("{ input test | summary test | attribute test; }",
+ createField("test", DataType.INT, "{ summary | index }"));
+ }
+
+ @Test
+ public void testStringAttributeSummaryRewriting() {
+ assertIndexingScript("{ input test | summary test | attribute test; }",
+ createField("test", DataType.STRING, "{ summary | attribute }"));
+ }
+
+ @Test
+ public void testMultiblockTokenize() {
+ SDField field = createField("test", DataType.STRING,
+ "{ input test | tokenize | { summary test; }; }");
+ assertIndexingScript("{ input test | tokenize | { summary test; }; }", field);
+ }
+
+ @Test
+ public void requireThatOutputDefaultsToCurrentField() {
+ assertIndexingScript("{ input test | attribute test; }",
+ createField("test", DataType.STRING, "{ attribute; }"));
+ assertIndexingScript("{ input test | tokenize normalize stem:\"SHORTEST\" | index test; }",
+ createField("test", DataType.STRING, "{ index; }"));
+ assertIndexingScript("{ input test | summary test; }",
+ createField("test", DataType.STRING, "{ summary; }"));
+ }
+
+ @Test
+ public void testTokenizeComparisonDisregardsConfig() {
+ assertIndexingScript("{ input test | tokenize normalize stem:\"SHORTEST\" | summary test | index test; }",
+ createField("test", DataType.STRING, "{ summary | tokenize | index; }"));
+ }
+
+ @Test
+ public void testDerivingFromSimple() throws Exception {
+ assertIndexing(Arrays.asList("clear_state | guard { input access | attribute access; }",
+ "clear_state | guard { input category | split \";\" | attribute category_arr; }",
+ "clear_state | guard { input category | tokenize | index category; }",
+ "clear_state | guard { input categories_src | lowercase | normalize | tokenize normalize stem:\"SHORTEST\" | index categories; }",
+ "clear_state | guard { input categoriesagain_src | lowercase | normalize | tokenize normalize stem:\"SHORTEST\" | index categoriesagain; }",
+ "clear_state | guard { input chatter | tokenize normalize stem:\"SHORTEST\" | index chatter; }",
+ "clear_state | guard { input description | tokenize normalize stem:\"SHORTEST\" | summary description | summary dyndesc | index description; }",
+ "clear_state | guard { input exactemento_src | lowercase | tokenize normalize stem:\"SHORTEST\" | index exactemento | summary exactemento; }",
+ "clear_state | guard { input longdesc | tokenize normalize stem:\"SHORTEST\" | summary dyndesc2 | summary dynlong | summary longdesc | summary longstat; }",
+ "clear_state | guard { input measurement | attribute measurement | summary measurement; }",
+ "clear_state | guard { input measurement | to_array | attribute measurement_arr; }",
+ "clear_state | guard { input popularity | attribute popularity; }",
+ "clear_state | guard { input popularity * input measurement | attribute popsiness; }",
+ "clear_state | guard { input smallattribute | attribute smallattribute; }",
+ "clear_state | guard { input title | tokenize normalize stem:\"SHORTEST\" | summary title | index title; }",
+ "clear_state | guard { input title . \" \" . input category | tokenize | summary exact | index exact; }"),
+ SearchBuilder.buildFromFile("src/test/examples/simple.sd"));
+ }
+
+ @Test
+ public void testIndexRewrite() throws Exception {
+ assertIndexing(
+ Arrays.asList("clear_state | guard { input title_src | lowercase | normalize | " +
+ " tokenize | index title; }",
+ "clear_state | guard { input title_src | summary title_s; }"),
+ SearchBuilder.buildFromFile("src/test/examples/indexrewrite.sd"));
+ }
+
+ @Test
+ public void requireThatPredicateFieldsGetOptimization() {
+ assertIndexingScript("{ 10 | set_var arity | { input test | optimize_predicate | attribute test; }; }",
+ createPredicateField(
+ "test", DataType.PREDICATE, "{ attribute; }", 10, OptionalLong.empty(), OptionalLong.empty()));
+ assertIndexingScript("{ 10 | set_var arity | { input test | optimize_predicate | summary test | attribute test; }; }",
+ createPredicateField(
+ "test", DataType.PREDICATE, "{ summary | attribute ; }", 10, OptionalLong.empty(), OptionalLong.empty()));
+ assertIndexingScript(
+ "{ 2 | set_var arity | 0L | set_var lower_bound | 1023L | set_var upper_bound | " +
+ "{ input test | optimize_predicate | attribute test; }; }",
+ createPredicateField("test", DataType.PREDICATE, "{ attribute; }", 2, OptionalLong.of(0L), OptionalLong.of(1023L)));
+ }
+
+ private static void assertIndexingScript(String expectedScript, SDField unprocessedField) {
+ assertEquals(expectedScript,
+ processField(unprocessedField).toString());
+ }
+
+ private static ScriptExpression processField(SDField unprocessedField) {
+ SDDocumentType sdoc = new SDDocumentType("test");
+ sdoc.addField(unprocessedField);
+ Search search = new Search("test", null);
+ search.addDocument(sdoc);
+ Processing.process(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles());
+ return unprocessedField.getIndexingScript();
+ }
+
+ private static SDField createField(String name, DataType type, String script) {
+ SDField field = new SDField(null, name, type);
+ field.parseIndexingScript(script);
+ return field;
+ }
+
+ private static SDField createPredicateField(
+ String name, DataType type, String script, int arity, OptionalLong lower_bound, OptionalLong upper_bound) {
+ SDField field = new SDField(null, name, type);
+ field.parseIndexingScript(script);
+ Index index = new Index("foo");
+ index.setBooleanIndexDefiniton(new BooleanIndexDefinition(
+ OptionalInt.of(arity), lower_bound, upper_bound, OptionalDouble.empty()));
+ field.addIndex(index);
+ return field;
+ }
+
+ private static SummaryField createDynamicSummaryField(SDField field, String name) {
+ return createSummaryField(field, name, true);
+ }
+
+ private static SummaryField createStaticSummaryField(SDField field, String name) {
+ return createSummaryField(field, name, false);
+ }
+
+ private static SummaryField createSummaryField(SDField field, String name, boolean dynamic) {
+ SummaryField summaryField = new SummaryField(name, field.getDataType());
+ if (dynamic) {
+ summaryField.setTransform(SummaryTransform.DYNAMICTEASER);
+ }
+ summaryField.addDestination("default");
+ summaryField.addSource(field.getName());
+ return summaryField;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValidationTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValidationTestCase.java
new file mode 100644
index 00000000000..8218d06f781
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValidationTestCase.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.derived.AbstractExportingTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import static com.yahoo.searchdefinition.processing.AssertIndexingScript.assertIndexing;
+import static com.yahoo.searchdefinition.processing.AssertSearchBuilder.assertBuildFails;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class IndexingValidationTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void testAttributeChanged() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_attribute_changed.sd",
+ "For search 'indexing_attribute_changed', field 'foo': For expression 'attribute foo': " +
+ "Attempting to assign conflicting values to field 'foo'.");
+ }
+
+ @Test
+ public void testAttributeOther() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_attribute_other.sd",
+ "For search 'indexing_attribute_other', field 'foo': Indexing expression 'attribute bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void testIndexChanged() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_index_changed.sd",
+ "For search 'indexing_index_changed', field 'foo': For expression 'index foo': " +
+ "Attempting to assign conflicting values to field 'foo'.");
+ }
+
+ @Test
+ public void testIndexOther() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_index_other.sd",
+ "For search 'indexing_index_other', field 'foo': Indexing expression 'index bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void testSummaryChanged() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_summary_changed.sd",
+ "For search 'indexing_summary_fail', field 'foo': For expression 'summary foo': Attempting " +
+ "to assign conflicting values to field 'foo'.");
+ }
+
+ @Test
+ public void testSummaryOther() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_summary_other.sd",
+ "For search 'indexing_summary_other', field 'foo': Indexing expression 'summary bar' " +
+ "attempts to write to a field other than 'foo'.");
+ }
+
+ @Test
+ public void testExtraField() throws IOException, ParseException {
+ assertIndexing(
+ Arrays.asList("clear_state | guard { input my_index | tokenize normalize stem:\"SHORTEST\" | index my_index | summary my_index }",
+ "clear_state | guard { input my_input | tokenize normalize stem:\"SHORTEST\" | index my_extra | summary my_extra }"),
+ SearchBuilder.buildFromFile("src/test/examples/indexing_extra.sd"));
+ }
+
+ @Test
+ public void requireThatMultilineOutputConflictThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_multiline_output_conflict.sd",
+ "For search 'indexing_multiline_output_confict', field 'cox': For expression 'index cox': " +
+ "Attempting to assign conflicting values to field 'cox'.");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValuesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValuesTestCase.java
new file mode 100644
index 00000000000..87ad5faf97c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IndexingValuesTestCase.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static com.yahoo.searchdefinition.processing.AssertSearchBuilder.assertBuildFails;
+import static com.yahoo.searchdefinition.processing.AssertSearchBuilder.assertBuilds;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class IndexingValuesTestCase {
+
+ @Test
+ public void requireThatModifyFieldNoOutputDoesNotThrow() throws IOException, ParseException {
+ assertBuilds("src/test/examples/indexing_modify_field_no_output.sd");
+ }
+
+ @Test
+ public void requireThatInputOtherFieldThrows() throws IOException, ParseException {
+ assertBuildFails("src/test/examples/indexing_input_other_field.sd",
+ "For search 'indexing_input_other_field', field 'bar': Indexing expression 'input foo' " +
+ "modifies the value of the document field 'bar'. This is no longer supported -- declare " +
+ "such fields outside the document.");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java
new file mode 100644
index 00000000000..0b401866932
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/IntegerIndex2AttributeTestCase.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.UnprocessingSearchBuilder;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+/**
+ * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a>
+ */
+public class IntegerIndex2AttributeTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testIntegerIndex2Attribute() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/integerindex2attribute.sd");
+ search.process();
+ new IntegerIndex2Attribute(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process();
+
+ SDField f;
+ f = search.getField("s1");
+ assertTrue(f.getAttributes().isEmpty());
+ assertTrue(f.existsIndex("s1"));
+ f = search.getField("s2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertTrue(f.existsIndex("s2"));
+
+ f = search.getField("as1");
+ assertTrue(f.getAttributes().isEmpty());
+ assertTrue(f.existsIndex("as1"));
+ f = search.getField("as2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertTrue(f.existsIndex("as2"));
+
+ f = search.getField("i1");
+ assertEquals(f.getAttributes().size(), 1);
+ assertTrue( ! f.existsIndex("i1"));
+
+ f = search.getField("i2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertTrue( ! f.existsIndex("i2"));
+
+ f = search.getField("ai1");
+ assertEquals(search.getField("ai1").getAttributes().size(), 1);
+ assertTrue( ! search.getField("ai1").existsIndex("ai1"));
+ f = search.getField("ai2");
+ assertEquals(f.getAttributes().size(), 1);
+ assertTrue( ! f.existsIndex("ai2"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidatorTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidatorTestCase.java
new file mode 100644
index 00000000000..d92a0f0aa54
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/MatchPhaseSettingsValidatorTestCase.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import org.junit.Test;
+
+import static com.yahoo.searchdefinition.processing.AssertSearchBuilder.assertBuildFails;
+
+public class MatchPhaseSettingsValidatorTestCase {
+
+ private static String getMessagePrefix() {
+ return "In search definition 'test', rank-profile 'default': match-phase attribute 'foo' ";
+ }
+
+ @Test
+ public void requireThatAttributeMustExists() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/non_existing_attribute.sd",
+ getMessagePrefix() + "does not exists");
+ }
+
+ @Test
+ public void requireThatAttributeMustBeNumeric() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/wrong_data_type_attribute.sd",
+ getMessagePrefix() + "must be single value numeric, but it is 'string'");
+ }
+
+ @Test
+ public void requireThatAttributeMustBeSingleValue() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/wrong_collection_type_attribute.sd",
+ getMessagePrefix() + "must be single value numeric, but it is 'Array<int>'");
+ }
+
+ @Test
+ public void requireThatAttributeMustHaveFastSearch() throws Exception {
+ assertBuildFails("src/test/examples/matchphase/non_fast_search_attribute.sd",
+ getMessagePrefix() + "must be fast-search, but it is not");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/NGramTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/NGramTestCase.java
new file mode 100644
index 00000000000..6dccdc235b9
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/NGramTestCase.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.document.Matching;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.searchdefinition.document.Stemming;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.*;
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class NGramTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testNGram() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/ngram.sd");
+ assertNotNull(search);
+
+ SDField gram1=search.getField("gram_1");
+ assertEquals(Matching.Type.GRAM,gram1.getMatching().getType());
+ assertEquals(1,gram1.getMatching().getGramSize());
+
+ SDField gram2=search.getField("gram_2");
+ assertEquals(Matching.Type.GRAM,gram2.getMatching().getType());
+ assertEquals(-1,gram2.getMatching().getGramSize()); // Not set explicitly
+
+ SDField gram3=search.getField("gram_3");
+ assertEquals(Matching.Type.GRAM,gram3.getMatching().getType());
+ assertEquals(3,gram3.getMatching().getGramSize());
+
+ assertEquals("input gram_1 | ngram 1 | index gram_1 | summary gram_1",gram1.getIndexingScript().iterator().next().toString());
+ assertEquals("input gram_2 | ngram 2 | index gram_2",gram2.getIndexingScript().iterator().next().toString());
+ assertEquals("input gram_3 | ngram 3 | index gram_3",gram3.getIndexingScript().iterator().next().toString());
+
+ assertFalse(gram1.getNormalizing().doRemoveAccents());
+ assertEquals(Stemming.NONE,gram1.getStemming());
+
+ List<String> queryCommands=gram1.getQueryCommands();
+ assertEquals(1,queryCommands.size());
+ assertEquals("ngram 1",queryCommands.get(0));
+ }
+
+ @Test
+ public void testInvalidNGramSetting1() throws IOException, ParseException {
+ try {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/invalidngram1.sd");
+ fail("Should cause an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("gram-size can only be set when the matching mode is 'gram'",e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInvalidNGramSetting2() throws IOException, ParseException {
+ try {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/invalidngram2.sd");
+ fail("Should cause an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("gram-size can only be set when the matching mode is 'gram'",e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInvalidNGramSetting3() throws IOException, ParseException {
+ try {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/invalidngram3.sd");
+ fail("Should cause an exception");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("gram matching is not supported with attributes, use 'index' not 'attribute' in indexing",e.getMessage());
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/PositionTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/PositionTestCase.java
new file mode 100644
index 00000000000..45dc5b3dc78
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/PositionTestCase.java
@@ -0,0 +1,127 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.document.DataType;
+import com.yahoo.document.PositionDataType;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.documentmodel.SummaryField;
+import com.yahoo.vespa.documentmodel.SummaryTransform;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test Position processor.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class PositionTestCase {
+
+ @Test
+ public void requireThatPositionCanBeAttribute() throws Exception {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/position_attribute.sd");
+ assertNull(search.getAttribute("pos"));
+ assertNull(search.getAttribute("pos.x"));
+ assertNull(search.getAttribute("pos.y"));
+
+ assertPositionAttribute(search, "pos", Attribute.CollectionType.SINGLE);
+ assertPositionSummary(search, "pos", false);
+ }
+
+ @Test
+ public void requireThatPositionCanNotBeIndex() throws Exception {
+ try {
+ SearchBuilder.buildFromFile("src/test/examples/position_index.sd");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("For search 'position_index', field 'pos': Indexing of data type 'position' is not " +
+ "supported, replace 'index' statement with 'attribute'.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatSummaryAloneDoesNotCreateZCurve() throws Exception {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/position_summary.sd");
+ assertNull(search.getAttribute("pos"));
+ assertNull(search.getAttribute("pos.x"));
+ assertNull(search.getAttribute("pos.y"));
+ assertNull(search.getAttribute("pos.zcurve"));
+
+ SummaryField summary = search.getSummaryField("pos");
+ assertNotNull(summary);
+ assertEquals(2, summary.getSourceCount());
+ Iterator<SummaryField.Source> it = summary.getSources().iterator();
+ assertEquals("pos.x", it.next().getName());
+ assertEquals("pos.y", it.next().getName());
+ assertEquals(SummaryTransform.NONE, summary.getTransform());
+
+ assertNull(search.getSummaryField("pos_ext.distance"));
+ }
+
+ @Test
+ public void requireThatExtraFieldCanBePositionAttribute() throws Exception {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/position_extra.sd");
+ assertNull(search.getAttribute("pos_ext"));
+ assertNull(search.getAttribute("pos_ext.x"));
+ assertNull(search.getAttribute("pos_ext.y"));
+
+ assertPositionAttribute(search, "pos_ext", Attribute.CollectionType.SINGLE);
+ assertPositionSummary(search, "pos_ext", false);
+ }
+
+ @Test
+ public void requireThatPositionArrayIsSupported() throws Exception {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/position_array.sd");
+ assertNull(search.getAttribute("pos"));
+ assertNull(search.getAttribute("pos.x"));
+ assertNull(search.getAttribute("pos.y"));
+
+ assertPositionAttribute(search, "pos", Attribute.CollectionType.ARRAY);
+ assertPositionSummary(search, "pos", true);
+ }
+
+ private static void assertPositionAttribute(Search search, String fieldName, Attribute.CollectionType type) {
+ Attribute attribute = search.getAttribute(PositionDataType.getZCurveFieldName(fieldName));
+ assertNotNull(attribute);
+ assertTrue(attribute.isPosition());
+ assertTrue(attribute.getCollectionType().equals(type));
+ assertTrue(attribute.getType().equals(Attribute.Type.LONG));
+ }
+
+ private static void assertPositionSummary(Search search, String fieldName, boolean isArray) {
+ assertSummaryField(search,
+ fieldName,
+ PositionDataType.getZCurveFieldName(fieldName),
+ (isArray ? DataType.getArray(PositionDataType.INSTANCE) : PositionDataType.INSTANCE),
+ SummaryTransform.GEOPOS);
+ assertSummaryField(search,
+ PositionDataType.getDistanceSummaryFieldName(fieldName),
+ PositionDataType.getZCurveFieldName(fieldName),
+ DataType.INT,
+ SummaryTransform.DISTANCE);
+ assertSummaryField(search,
+ PositionDataType.getPositionSummaryFieldName(fieldName),
+ PositionDataType.getZCurveFieldName(fieldName),
+ DataType.getArray(DataType.STRING),
+ SummaryTransform.POSITIONS);
+ }
+
+ private static void assertSummaryField(Search search, String fieldName, String sourceName, DataType dataType,
+ SummaryTransform transform)
+ {
+ SummaryField summary = search.getSummaryField(fieldName);
+ assertNotNull(summary);
+ assertEquals(1, summary.getSourceCount());
+ assertEquals(sourceName, summary.getSingleSource());
+ assertEquals(dataType, summary.getDataType());
+ assertEquals(transform, summary.getTransform());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankModifierTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankModifierTestCase.java
new file mode 100644
index 00000000000..9778ad20374
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankModifierTestCase.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests for the field "rank {" shortcut
+ * @author vegardh
+ *
+ */
+public class RankModifierTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testLiteral() throws IOException, ParseException {
+ Search search = SearchBuilder.buildFromFile("src/test/examples/rankmodifier/literal.sd");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankPropertyVariablesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankPropertyVariablesTestCase.java
new file mode 100644
index 00000000000..d3ab9aaec1e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankPropertyVariablesTestCase.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.RankProfile.RankProperty;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.fail;
+
+public class RankPropertyVariablesTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testRankPropVariables() throws IOException, ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ Search search = SearchBuilder.buildFromFile("src/test/examples/rankpropvars.sd", new BaseDeployLogger(), rankProfileRegistry);
+ assertRankPropEquals(rankProfileRegistry.getRankProfile(search, "other").getRankProperties(), "$testvar1", "foo");
+ assertRankPropEquals(rankProfileRegistry.getRankProfile(search, "other").getRankProperties(), "$testvar_2", "bar");
+ assertRankPropEquals(rankProfileRegistry.getRankProfile(search, "other").getRankProperties(), "$testvarOne23", "baz");
+ assertRankPropEquals(rankProfileRegistry.getRankProfile(search, "another").getRankProperties(), "$Testvar1", "1");
+ assertRankPropEquals(rankProfileRegistry.getRankProfile(search, "another").getRankProperties(), "$Testvar_4", "4");
+ assertRankPropEquals(rankProfileRegistry.getRankProfile(search, "another").getRankProperties(), "$testvarFour23", "234234.234");
+ }
+
+ private void assertRankPropEquals(List<RankProperty> props, String key, String val) {
+ for (RankProperty prop : props) {
+ if (prop.getName().equals(key)) {
+ if (prop.getValue().equals(val)) {
+ return;
+ }
+ }
+ }
+ fail(key+":"+val+ " not found in rank properties.");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionWithTensorTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionWithTensorTestCase.java
new file mode 100644
index 00000000000..f33106b32bd
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionWithTensorTestCase.java
@@ -0,0 +1,220 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.RankProfile;
+import com.yahoo.searchdefinition.RankProfileRegistry;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class RankingExpressionWithTensorTestCase {
+
+ private static class SearchFixture {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ Search search;
+ SearchFixture(String rankProfiles) throws ParseException {
+ SearchBuilder builder = new SearchBuilder(rankProfileRegistry);
+ String sdContent = "search test {\n" +
+ " document test {\n" +
+ " }\n" +
+ rankProfiles +
+ "\n" +
+ "}";
+ builder.importString(sdContent);
+ builder.build();
+ search = builder.getSearch();
+ }
+ public void assertFirstPhaseExpression(String expExpression, String rankProfile) {
+ assertEquals(expExpression, getRankProfile(rankProfile).getFirstPhaseRanking().getRoot().toString());
+ }
+ public void assertSecondPhaseExpression(String expExpression, String rankProfile) {
+ assertEquals(expExpression, getRankProfile(rankProfile).getSecondPhaseRanking().getRoot().toString());
+ }
+ public void assertRankProperty(String expValue, String name, String rankProfile) {
+ List<RankProfile.RankProperty> rankPropertyList = getRankProfile(rankProfile).getRankPropertyMap().get(name);
+ assertEquals(1, rankPropertyList.size());
+ assertEquals(expValue, rankPropertyList.get(0).getValue());
+ }
+ public void assertMacro(String expExpression, String macroName, String rankProfile) {
+ assertEquals(expExpression, getRankProfile(rankProfile).getMacros().get(macroName).getRankingExpression().getRoot().toString());
+ }
+ private RankProfile getRankProfile(String rankProfile) {
+ return rankProfileRegistry.getRankProfile(search, rankProfile).compile();
+ }
+ }
+
+ @Test
+ public void requireThatExpressionWithSingleLineTensorCanBeParsed() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum({ {x:1}:1, {x:2,y:1}:2 })\n" +
+ " }\n" +
+ " }");
+ f.assertFirstPhaseExpression("sum({{x:1}:1.0,{x:2,y:1}:2.0})", "my_profile");
+ }
+
+ @Test
+ public void requireThatExpressionWithMultiLineTensorCanBeParsed() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression {\n" +
+ " sum({ {x:1}:1,\n" +
+ " {x:2,y:1}:2 })\n" +
+ " }\n" +
+ " }\n" +
+ " }");
+ f.assertFirstPhaseExpression("sum({{x:1}:1.0,{x:2,y:1}:2.0})", "my_profile");
+ }
+
+ @Test
+ public void requireThatSingleLineConstantTensorAndTypeCanBeParsed() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor {\n" +
+ " value: { {x:1}:1, {x:2,y:1}:2 }\n" +
+ " type: tensor(x{},y{})\n" +
+ " }\n" +
+ " }\n" +
+ " }");
+ f.assertFirstPhaseExpression("sum(constant(my_tensor))", "my_profile");
+ f.assertRankProperty("{{x:1}:1.0,{x:2,y:1}:2.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{},y{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatMultiLineConstantTensorAndTypeCanBeParsed() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor {\n" +
+ " value {\n" +
+ " { {x:1}:1,\n" +
+ " {x:2,y:1}:2 }\n" +
+ " }\n" +
+ " type: tensor(x{},y{})\n" +
+ " }\n" +
+ " }\n" +
+ " }");
+ f.assertFirstPhaseExpression("sum(constant(my_tensor))", "my_profile");
+ f.assertRankProperty("{{x:1}:1.0,{x:2,y:1}:2.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor(x{},y{})", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatConstantTensorsCanBeUsedInSecondPhaseExpression() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " second-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor {\n" +
+ " value: { {x:1}:1 }\n" +
+ " }\n" +
+ " }\n" +
+ " }");
+ f.assertSecondPhaseExpression("sum(constant(my_tensor))", "my_profile");
+ f.assertRankProperty("{{x:1}:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatConstantTensorsCanBeUsedInInheritedRankProfile() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile parent {\n" +
+ " constants {\n" +
+ " my_tensor {\n" +
+ " value: { {x:1}:1 }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile my_profile inherits parent {\n" +
+ " first-phase {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " }");
+ f.assertFirstPhaseExpression("sum(constant(my_tensor))", "my_profile");
+ f.assertRankProperty("{{x:1}:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatConstantTensorsCanBeUsedInMacro() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " macro my_macro() {\n" +
+ " expression: sum(my_tensor)\n" +
+ " }\n" +
+ " first-phase {\n" +
+ " expression: 5.0 + my_macro\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_tensor {\n" +
+ " value: { {x:1}:1 }\n" +
+ " }\n" +
+ " }\n" +
+ " }");
+ f.assertFirstPhaseExpression("5.0 + my_macro", "my_profile");
+ f.assertMacro("sum(constant(my_tensor))", "my_macro", "my_profile");
+ f.assertRankProperty("{{x:1}:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Test
+ public void requireThatCombinationOfConstantTensorsAndConstantValuesCanBeUsed() throws ParseException {
+ SearchFixture f = new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " first-phase {\n" +
+ " expression: my_number_1 + sum(my_tensor) + my_number_2\n" +
+ " }\n" +
+ " constants {\n" +
+ " my_number_1: 3.0\n" +
+ " my_tensor {\n" +
+ " value: { {x:1}:1 }\n" +
+ " }\n" +
+ " my_number_2: 5.0\n" +
+ " }\n" +
+ " }");
+ f.assertFirstPhaseExpression("3.0 + sum(constant(my_tensor)) + 5.0", "my_profile");
+ f.assertRankProperty("{{x:1}:1.0}", "constant(my_tensor).value", "my_profile");
+ f.assertRankProperty("tensor", "constant(my_tensor).type", "my_profile");
+ }
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void requireThatInvalidTensorTypeSpecThrowsException() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For constant tensor 'my_tensor' in rank profile 'my_profile': Illegal tensor type spec: Failed parsing element 'x' in type spec 'tensor(x)'");
+ new SearchFixture(
+ " rank-profile my_profile {\n" +
+ " constants {\n" +
+ " my_tensor {\n" +
+ " value: { {x:1}:1 }\n" +
+ " type: tensor(x)\n" +
+ " }\n" +
+ " }\n" +
+ " }");
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionsTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionsTestCase.java
new file mode 100644
index 00000000000..083627b1211
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/RankingExpressionsTestCase.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.*;
+import com.yahoo.searchdefinition.derived.DerivedConfiguration;
+import com.yahoo.searchdefinition.derived.AttributeFields;
+import com.yahoo.searchdefinition.derived.RawRankProfile;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+
+// TODO: WHO?
+public class RankingExpressionsTestCase extends SearchDefinitionTestCase {
+
+ @Test
+ public void testMacros() throws IOException, ParseException {
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ Search search = SearchBuilder.createFromDirectory("src/test/examples/rankingexpressionfunction", rankProfileRegistry).getSearch();
+ final RankProfile macrosRankProfile = rankProfileRegistry.getRankProfile(search, "macros");
+ macrosRankProfile.parseExpressions();
+ final Map<String, RankProfile.Macro> macros = macrosRankProfile.getMacros();
+ assertEquals(2, macros.get("titlematch$").getFormalParams().size());
+ assertEquals("var1", macros.get("titlematch$").getFormalParams().get(0));
+ assertEquals("var2", macros.get("titlematch$").getFormalParams().get(1));
+ assertEquals("var1 * var2 + 890", macros.get("titlematch$").getTextualExpression().trim());
+ assertEquals("var1 * var2 + 890", macros.get("titlematch$").getRankingExpression().getRoot().toString());
+ assertEquals("0.8+0.2*titlematch$(4,5)+0.8*titlematch$(7,8)*closeness(distance)", macrosRankProfile.getFirstPhaseRankingString().trim());
+ assertEquals("78 + closeness(distance)", macros.get("artistmatch").getTextualExpression().trim());
+ assertEquals(0, macros.get("artistmatch").getFormalParams().size());
+
+ List<Map.Entry<String, Object>> rankProperties = new ArrayList<>(new RawRankProfile(macrosRankProfile, new AttributeFields(search)).configProperties().entrySet());
+ assertEquals(6, rankProperties.size());
+
+ assertEquals("rankingExpression(titlematch$).rankingScript.part0", rankProperties.get(0).getKey());
+ assertEquals("var1 * var2 + 890", rankProperties.get(0).getValue());
+
+ assertEquals("rankingExpression(artistmatch).rankingScript.part1", rankProperties.get(1).getKey());
+ assertEquals("78 + closeness(distance)", rankProperties.get(1).getValue());
+
+ assertEquals("rankingExpression(firstphase).rankingScript", rankProperties.get(5).getKey());
+ assertEquals("0.8 + 0.2 * rankingExpression(titlematch$@126063073eb2deb.ab95cd69909927c) + 0.8 * rankingExpression(titlematch$@c7e4c2d0e6d9f2a1.1d4ed08e56cce2e6) * closeness(distance)", rankProperties.get(5).getValue());
+
+ assertEquals("rankingExpression(titlematch$@c7e4c2d0e6d9f2a1.1d4ed08e56cce2e6).rankingScript.part3", rankProperties.get(3).getKey());
+ assertEquals("7 * 8 + 890", rankProperties.get(3).getValue());
+
+ assertEquals("rankingExpression(titlematch$@126063073eb2deb.ab95cd69909927c).rankingScript.part2", rankProperties.get(2).getKey());
+ assertEquals("4 * 5 + 890", rankProperties.get(2).getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testThatIncludingFileInSubdirFails() throws IOException, ParseException {
+ RankProfileRegistry registry = new RankProfileRegistry();
+ Search search = SearchBuilder.createFromDirectory("src/test/examples/rankingexpressioninfile", registry).getSearch();
+ new DerivedConfiguration(search, registry); // rank profile parsing happens during deriving
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/ReservedDocumentNamesTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ReservedDocumentNamesTestCase.java
new file mode 100644
index 00000000000..cab6b94fa92
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/ReservedDocumentNamesTestCase.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.derived.AbstractExportingTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ReservedDocumentNamesTestCase extends AbstractExportingTestCase {
+
+ @Test
+ public void requireThatPositionIsAReservedDocumentName() throws IOException, ParseException {
+ try {
+ assertCorrectDeriving("reserved_position");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("For search 'position': Document name 'position' is reserved.", e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java
new file mode 100644
index 00000000000..c5ac1ae1ed3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/SummaryFieldsMustHaveValidSourceTestCase.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.searchdefinition.*;
+
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class SummaryFieldsMustHaveValidSourceTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void requireThatInvalidSourceIsCaught() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/invalidsummarysource.sd");
+ search.process();
+ try {
+ new SummaryFieldsMustHaveValidSource(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process();
+ assertTrue("This should throw and never get here", false);
+ } catch (IllegalArgumentException e) {
+ assertEquals("For search 'invalidsummarysource', summary class 'baz', summary field 'cox': there is no valid source 'nonexistingfield'.", e.getMessage());
+ }
+ }
+ @Test
+ public void requireThatInvalidImplicitSourceIsCaught() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/invalidimplicitsummarysource.sd");
+ search.process();
+ try {
+ new SummaryFieldsMustHaveValidSource(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process();
+ assertTrue("This should throw and never get here", false);
+ } catch (IllegalArgumentException e) {
+ assertEquals("For search 'invalidsummarysource', summary class 'baz', summary field 'cox': there is no valid source 'cox'.", e.getMessage());
+ }
+ }
+ @Test
+ public void requireThatInvalidSelfReferingSingleSource() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/invalidselfreferringsummary.sd");
+ search.process();
+ try {
+ new SummaryFieldsMustHaveValidSource(search, new BaseDeployLogger(), new RankProfileRegistry(), new QueryProfiles()).process();
+ assertTrue("This should throw and never get here", false);
+ } catch (IllegalArgumentException e) {
+ assertEquals("For search 'invalidselfreferringsummary', summary class 'withid', summary field 'w': there is no valid source 'w'.", e.getMessage());
+ }
+ }
+ @Test
+ public void requireThatDocumentIdIsAllowedToPass() throws IOException, ParseException {
+ Search search = UnprocessingSearchBuilder.buildUnprocessedFromFile("src/test/examples/documentidinsummary.sd");
+ search.process();
+ BaseDeployLogger deployLogger = new BaseDeployLogger();
+ RankProfileRegistry rankProfileRegistry = new RankProfileRegistry();
+ new SummaryFieldsMustHaveValidSource(search, deployLogger, rankProfileRegistry, new QueryProfiles()).process();
+ assertEquals("documentid", search.getSummary("withid").getSummaryField("w").getSingleSource());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorFieldTestCase.java b/config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorFieldTestCase.java
new file mode 100644
index 00000000000..ed29d086bb1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/searchdefinition/processing/TensorFieldTestCase.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.searchdefinition.processing;
+
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+/**
+ * @author <a href="geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class TensorFieldTestCase {
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void requireThatTensorFieldCannotBeOfCollectionType() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For search 'test', field 'f1': A field with collection type of tensor is not supported. Use simple type 'tensor' instead.");
+ SearchBuilder.createFromString(getSd("field f1 type array<tensor> {}"));
+ }
+
+ @Test
+ public void requireThatTensorFieldCannotBeIndexField() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For search 'test', field 'f1': A field of type 'tensor' cannot be specified as an 'index' field.");
+ SearchBuilder.createFromString(getSd("field f1 type tensor { indexing: index }"));
+ }
+
+ @Test
+ public void requireThatTensorAttributeCannotBeFastSearch() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For search 'test', field 'f1': An attribute of type 'tensor' cannot be 'fast-search'.");
+ SearchBuilder.createFromString(getSd("field f1 type tensor { indexing: attribute \n attribute: fast-search }"));
+ }
+
+ @Test
+ public void requireThatIllegalTensorTypeSpecThrowsException() throws ParseException {
+ exception.expect(IllegalArgumentException.class);
+ exception.expectMessage("For attribute field 'f1': Illegal tensor type spec: Failed parsing element 'invalid' in type spec 'tensor(invalid)'");
+ SearchBuilder.createFromString(getSd("field f1 type tensor { indexing: attribute \n attribute: tensor(invalid) }"));
+ }
+
+ private static String getSd(String field) {
+ return "search test {\n document test {\n" + field + "}\n}\n";
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/documentmodel/DocumentModelBuilderTestCase.java b/config-model/src/test/java/com/yahoo/vespa/documentmodel/DocumentModelBuilderTestCase.java
new file mode 100644
index 00000000000..05bf774b1d0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/documentmodel/DocumentModelBuilderTestCase.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.documentmodel;
+
+import com.yahoo.document.DocumenttypesConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.SearchDefinitionTestCase;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.configmodel.producers.DocumentManager;
+import com.yahoo.vespa.configmodel.producers.DocumentTypes;
+import org.junit.Test;
+import java.io.IOException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class DocumentModelBuilderTestCase extends SearchDefinitionTestCase {
+ @Test
+ public void testDocumentManagerSimple() throws IOException, ParseException {
+ DocumentModel model = createAndTestModel("src/test/configmodel/types/types.sd");
+
+ DocumentmanagerConfig.Builder documentmanagerCfg = new DocumentManager().produce(model, new DocumentmanagerConfig.Builder());
+ assertConfigFile("src/test/configmodel/types/documentmanager.cfg",
+ new DocumentmanagerConfig(documentmanagerCfg).toString());
+ }
+ @Test
+ // This is ignored as enums in config are not testable in this way. See bug 4748050
+ public void testDocumentTypesSimple() throws IOException, ParseException {
+ DocumentModel model = createAndTestModel("src/test/configmodel/types/types.sd");
+
+ DocumenttypesConfig.Builder documenttypesCfg = new DocumentTypes().produce(model, new DocumenttypesConfig.Builder());
+ assertConfigFile("src/test/configmodel/types/documenttypes.cfg",
+ new DocumenttypesConfig(documenttypesCfg).toString());
+ }
+
+ @Test
+ public void testDocumentTypesWithDocumentField() throws IOException, ParseException {
+ SearchBuilder search = new SearchBuilder();
+ search.importFile("src/test/configmodel/types/other_doc.sd");
+ search.importFile("src/test/configmodel/types/type_with_doc_field.sd");
+ search.build();
+ DocumentModel model = search.getModel();
+
+ DocumenttypesConfig.Builder documenttypesCfg = new DocumentTypes().produce(model, new DocumenttypesConfig.Builder());
+ assertConfigFile("src/test/configmodel/types/documenttypes_with_doc_field.cfg",
+ new DocumenttypesConfig(documenttypesCfg).toString());
+ }
+
+ @Test
+ public void testMultipleInheritanceArray() throws IOException, ParseException {
+ SearchBuilder search = new SearchBuilder();
+ search.importFile("src/test/cfg/search/data/travel/searchdefinitions/TTData.sd");
+ search.importFile("src/test/cfg/search/data/travel/searchdefinitions/TTEdge.sd");
+ search.importFile("src/test/cfg/search/data/travel/searchdefinitions/TTPOI.sd");
+ search.build();
+ }
+
+ private DocumentModel createAndTestModel(String sd) throws IOException, ParseException {
+ SearchBuilder search = SearchBuilder.createFromFile(sd);
+ DocumentModel model = search.getModel();
+
+ assertEquals(2, model.getDocumentManager().getTypes().size());
+ assertNotNull(model.getDocumentManager().getDocumentType("document"));
+ assertNotNull(model.getDocumentManager().getDocumentType("types"));
+ return model;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/HostResourceTest.java b/config-model/src/test/java/com/yahoo/vespa/model/HostResourceTest.java
new file mode 100644
index 00000000000..31d45c3b75f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/HostResourceTest.java
@@ -0,0 +1,105 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.model.test.MockRoot;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ * @since 5.1.14
+ */
+public class HostResourceTest {
+
+ @Test
+ public void next_available_baseport_is_BASE_PORT_when_no_ports_have_been_reserved() {
+ HostResource host = newMockHostResource();
+ assertThat(host.nextAvailableBaseport(1), is(HostResource.BASE_PORT));
+ }
+
+ @Test
+ public void next_available_baseport_is_BASE_PORT_plus_one_when_one_port_has_been_reserved() {
+ HostResource host = newMockHostResource();
+ host.reservePort(new TestService(1), HostResource.BASE_PORT);
+ assertThat(host.nextAvailableBaseport(1), is(HostResource.BASE_PORT + 1));
+ }
+
+ @Test
+ public void no_available_baseport_when_service_requires_more_consecutive_ports_than_available() {
+ HostResource host = newMockHostResource();
+
+ for (int p = HostResource.BASE_PORT; p < HostResource.BASE_PORT + HostResource.MAX_PORTS; p += 2) {
+ host.reservePort(new TestService(1), p);
+ }
+ assertThat(host.nextAvailableBaseport(2), is(0));
+
+ try {
+ host.reservePort(new TestService(2), HostResource.BASE_PORT);
+ } catch (RuntimeException e) {
+ assertThat(e.getMessage(), containsString("Too many ports are reserved"));
+ }
+ }
+
+ @Test
+ public void require_exception_when_no_matching_hostalias() {
+ TestService service = new TestService(1);
+ try {
+ service.initService();
+ } catch (RuntimeException e) {
+ assertThat(e.getMessage(), endsWith("No host found for service 'hostresourcetest$testservice0'. " +
+ "The hostalias is probably missing from hosts.xml."));
+ }
+ }
+
+ @Test
+ public void port_above_vespas_port_range_can_be_reserved() {
+ HostResource host = newMockHostResource();
+ host.allocateService(new TestService(1), HostResource.BASE_PORT + HostResource.MAX_PORTS + 1);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void allocating_same_port_throws_exception() {
+ HostResource host = newMockHostResource();
+ TestService service1 = new TestService(1);
+ TestService service2 = new TestService(1);
+
+ host.allocateService(service1, HostResource.BASE_PORT);
+ host.allocateService(service2, HostResource.BASE_PORT);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void allocating_overlapping_ports_throws_exception() {
+ HostResource host = newMockHostResource();
+ TestService service2 = new TestService(2);
+ TestService service1 = new TestService(1);
+
+ host.allocateService(service2, HostResource.BASE_PORT);
+ host.allocateService(service1, HostResource.BASE_PORT + 1);
+ }
+
+
+ private HostResource newMockHostResource() {
+ return new HostResource(new Host(new MockRoot()));
+ }
+
+ private class TestService extends AbstractService {
+ private final int portCount;
+
+ TestService(int portCount) {
+ super("testService");
+ this.portCount = portCount;
+ }
+
+ @Override
+ public boolean requiresWantedPort() {
+ return true;
+ }
+
+ @Override
+ public int getPortCount() { return portCount; }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/InstanceResolverTest.java b/config-model/src/test/java/com/yahoo/vespa/model/InstanceResolverTest.java
new file mode 100644
index 00000000000..807e0b0ae5f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/InstanceResolverTest.java
@@ -0,0 +1,228 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.test.FunctionTestConfig;
+import com.yahoo.test.FunctionTestConfig.*;
+import com.yahoo.test.SimpletypesConfig;
+import com.yahoo.config.codegen.*;
+import com.yahoo.text.StringUtilities;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.junit.Assert.*;
+
+public class InstanceResolverTest {
+
+ @Test
+ public void testApplyDefToBuilder() throws Exception {
+ FunctionTestConfig.Builder builder = createVariableAccessBuilder();
+ InnerCNode targetDef = getDef(FunctionTestConfig.CONFIG_DEF_SCHEMA);
+
+ // Mutate the def, user has set different schema ...
+ ((LeafCNode) targetDef.getChild("stringwithdef")).setDefaultValue(new DefaultValue("newDef", new DefLine.Type("string")));
+ ((LeafCNode) targetDef.getChild("string_val")).setDefaultValue(new DefaultValue("newDefVal", new DefLine.Type("string")));
+ ((LeafCNode) targetDef.getChild("enumwithdef")).setDefaultValue(new DefaultValue(Enumwithdef.FOOBAR2.toString(), new DefLine.Type("enum")));
+ ((LeafCNode) targetDef.getChild("enum_val")).setDefaultValue(new DefaultValue(Enum_val.FOO.toString(), new DefLine.Type("enum")));
+ ((LeafCNode) targetDef.getChild("basicStruct").getChild("foo")).setDefaultValue(new DefaultValue("basicSchmasic", new DefLine.Type("string")));
+ ((LeafCNode) targetDef.getChild("basicStruct").getChild("bar")).setDefaultValue(new DefaultValue("89", new DefLine.Type("int")));
+ InnerCNode rootStruct = ((InnerCNode) targetDef.getChild("rootStruct"));
+ InnerCNode innerArr = (InnerCNode) rootStruct.getChild("innerArr");
+ ((LeafCNode) innerArr.getChild("boolVal")).setDefaultValue(new DefaultValue("true", new DefLine.Type("bool")));
+ ((LeafCNode) innerArr.getChild("stringVal")).setDefaultValue(new DefaultValue("derp", new DefLine.Type("string")));
+ InnerCNode myArray = ((InnerCNode) targetDef.getChild("myarray"));
+ myArray.children().put("intval", LeafCNode.newInstance(new DefLine.Type("int"), myArray, "intval", "-123424"));
+ targetDef.children().put("myarray", myArray);
+ InstanceResolver.applyDef(builder, targetDef);
+ FunctionTestConfig c = new FunctionTestConfig(builder);
+ assertEquals(c.string_val(), "foo");
+ assertEquals(c.stringwithdef(), "newDef");
+ assertEquals(c.enumwithdef(), Enumwithdef.FOOBAR2);
+ assertEquals(c.enum_val(), Enum_val.FOOBAR);
+ assertEquals(c.double_with_def(), -12, 0.0001);
+ assertEquals(c.basicStruct().foo(), "basicSchmasic");
+ assertEquals(c.basicStruct().bar(), 3);
+ assertTrue(c.rootStruct().innerArr(0).boolVal());
+ assertEquals(c.rootStruct().innerArr(0).stringVal(), "deep");
+ assertEquals(c.myarray(0).intval(), -123424);
+ }
+
+ /**
+ * Values unset on builder, trying to set them from def file, but type mismatches there
+ * @throws Exception
+ */
+ @Test
+ public void testApplyDefToBuilderMismatches() throws Exception {
+ FunctionTestConfig.Builder builder = createVariableAccessBuilderManyUnset();
+ InnerCNode targetDef = getDef(FunctionTestConfig.CONFIG_DEF_SCHEMA);
+
+ // Break the defs for these, they are unset on builder:
+ targetDef.children().put("stringwithdef", LeafCNode.newInstance(new DefLine.Type("int"), targetDef, "stringwithdef", "1"));
+ targetDef.children().put("int_val", LeafCNode.newInstance(new DefLine.Type("string"), targetDef, "int_val", "fooOO"));
+
+ InstanceResolver.applyDef(builder, targetDef);
+ try {
+ FunctionTestConfig c = new FunctionTestConfig(builder);
+ fail("No exception on incomplete builder");
+ } catch (Exception e) {
+ }
+ }
+
+ // copied from FunctionTest
+ private FunctionTestConfig.Builder createVariableAccessBuilder() {
+ return new FunctionTestConfig.Builder().
+ bool_val(false).
+ bool_with_def(true).
+ int_val(5).
+ int_with_def(-14).
+ long_val(12345678901L).
+ long_with_def(-9876543210L).
+ double_val(41.23).
+ double_with_def(-12).
+ string_val("foo").
+ //stringwithdef("bar").
+ enum_val(Enum_val.FOOBAR).
+ //enumwithdef(Enumwithdef.BAR2).
+ refval(":parent:").
+ refwithdef(":parent:").
+ fileVal("etc").
+ boolarr(false).
+ longarr(9223372036854775807L).
+ longarr(-9223372036854775808L).
+ doublearr(2344.0).
+ doublearr(123.0).
+ stringarr("bar").
+ enumarr(Enumarr.VALUES).
+ refarr(Arrays.asList(":parent:", ":parent", "parent:")). // test collection based setter
+ fileArr("bin").
+
+ basicStruct(new BasicStruct.Builder().
+ //foo("basicFoo").
+ bar(3).
+ intArr(310)).
+
+ rootStruct(new RootStruct.Builder().
+ inner0(new RootStruct.Inner0.Builder().
+ index(11)).
+ inner1(new RootStruct.Inner1.Builder().
+ index(12)).
+ innerArr(new RootStruct.InnerArr.Builder().
+ //boolVal(true).
+ stringVal("deep"))).
+
+ myarray(new Myarray.Builder().
+ //intval(-5).
+ stringval("baah").
+ stringval("yikes").
+ enumval(Myarray.Enumval.INNER).
+ refval(":parent:").
+ fileVal("file0").
+ anotherarray(new Myarray.Anotherarray.Builder().
+ foo(7)).
+ myStruct(new Myarray.MyStruct.Builder().
+ a(1).
+ b(2))).
+
+ myarray(new Myarray.Builder().
+ intval(5).
+ enumval(Myarray.Enumval.INNER).
+ refval(":parent:").
+ fileVal("file1").
+ anotherarray(new Myarray.Anotherarray.Builder().
+ foo(1).
+ foo(2)).
+ myStruct(new Myarray.MyStruct.Builder().
+ a(-1).
+ b(-2)));
+
+ }
+
+ private FunctionTestConfig.Builder createVariableAccessBuilderManyUnset() {
+ return new FunctionTestConfig.Builder().
+ bool_val(false).
+ bool_with_def(true).
+ //int_val(5).
+ int_with_def(-14).
+ long_val(12345678901L).
+ long_with_def(-9876543210L).
+ double_val(41.23).
+ double_with_def(-12).
+ string_val("foo").
+ //stringwithdef("bar").
+ enum_val(Enum_val.FOOBAR).
+ //enumwithdef(Enumwithdef.BAR2).
+ refval(":parent:").
+ refwithdef(":parent:").
+ fileVal("etc").
+ boolarr(false).
+ longarr(9223372036854775807L).
+ longarr(-9223372036854775808L).
+ doublearr(2344.0).
+ doublearr(123.0).
+ stringarr("bar").
+ enumarr(Enumarr.VALUES).
+ refarr(Arrays.asList(":parent:", ":parent", "parent:")). // test collection based setter
+ fileArr("bin").
+
+ basicStruct(new BasicStruct.Builder().
+ //foo("basicFoo").
+ bar(3).
+ intArr(310)).
+
+ rootStruct(new RootStruct.Builder().
+ inner0(new RootStruct.Inner0.Builder().
+ index(11)).
+ inner1(new RootStruct.Inner1.Builder().
+ index(12)).
+ innerArr(new RootStruct.InnerArr.Builder().
+ //boolVal(true).
+ stringVal("deep"))).
+
+ myarray(new Myarray.Builder().
+ intval(-5).
+ stringval("baah").
+ stringval("yikes").
+ enumval(Myarray.Enumval.INNER).
+ refval(":parent:").
+ fileVal("file0").
+ anotherarray(new Myarray.Anotherarray.Builder().
+ foo(7)).
+ myStruct(new Myarray.MyStruct.Builder().
+ a(1).
+ b(2))).
+
+ myarray(new Myarray.Builder().
+ intval(5).
+ enumval(Myarray.Enumval.INNER).
+ refval(":parent:").
+ fileVal("file1").
+ anotherarray(new Myarray.Anotherarray.Builder().
+ foo(1).
+ foo(2)).
+ myStruct(new Myarray.MyStruct.Builder().
+ a(-1).
+ b(-2)));
+
+ }
+
+ private InnerCNode getDef(String[] schema) {
+ ArrayList<String> def = new ArrayList<>();
+ def.addAll(Arrays.asList(schema));
+ return new DefParser("documentmanager",
+ new StringReader(StringUtilities.implode(def.toArray(new String[def.size()]), "\n"))).getTree();
+ }
+
+ @Test
+ public void testExtraFieldsAreIgnored() throws Exception {
+ try {
+ SimpletypesConfig.Builder builder = new SimpletypesConfig.Builder();
+ InnerCNode defWithExtra = new DefParser(SimpletypesConfig.CONFIG_DEF_NAME, new StringReader(StringUtilities.implode(SimpletypesConfig.CONFIG_DEF_SCHEMA, "\n") + "\nnewfield string default=\"foo\"\n")).getTree();
+ InstanceResolver.applyDef(builder, defWithExtra);
+ } catch (NoSuchFieldException e) {
+ fail("Should not fail on extra field");
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/RecentLogFilterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/RecentLogFilterTest.java
new file mode 100644
index 00000000000..c203efb3d94
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/RecentLogFilterTest.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
+
+/**
+ * @author musum
+ * @since 5.1
+ */
+public class RecentLogFilterTest {
+
+ @Test
+ public void basic() {
+ RecentLogFilter rlf = new RecentLogFilter();
+ List<LogRecord> logRecords = new ArrayList<>();
+ for (int i = 0; i < RecentLogFilter.maxMessages + 1; i++) {
+ logRecords.add(new LogRecord(Level.INFO, "" + i));
+ }
+
+ assertTrue(rlf.isLoggable(logRecords.get(0)));
+ assertFalse(rlf.isLoggable(logRecords.get(0)));
+
+ for (int i = 1; i < RecentLogFilter.maxMessages + 1; i++) {
+ assertTrue(rlf.isLoggable(logRecords.get(i)));
+ }
+ System.out.println(logRecords.size());
+ System.out.println(logRecords);
+
+ // Should have filled up maxMessages slots with records 1-maxMessages
+ // and pushed the first one out, so the below should return true
+ assertTrue(rlf.isLoggable(logRecords.get(0)));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/VespaModelFactoryTest.java b/config-model/src/test/java/com/yahoo/vespa/model/VespaModelFactoryTest.java
new file mode 100644
index 00000000000..05445642803
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/VespaModelFactoryTest.java
@@ -0,0 +1,257 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.MockModelContext;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.*;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.provision.*;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.internal.stubbing.answers.ThrowsExceptionClass;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+/**
+ * @author lulf
+ */
+public class VespaModelFactoryTest {
+
+ private ModelContext testModelContext;
+
+ @Before
+ public void setupContext() {
+ testModelContext = new MockModelContext();
+ }
+
+ @Test
+ public void testThatFactoryCanBuildModel() {
+ VespaModelFactory modelFactory = new VespaModelFactory(new NullConfigModelRegistry());
+ Model model = modelFactory.createModel(testModelContext);
+ assertNotNull(model);
+ assertTrue(model instanceof VespaModel);
+ }
+
+ // Uses an application package that throws IllegalArgumentException when validating
+ @Test(expected = IllegalArgumentException.class)
+ public void testThatFactoryModelValidationFailsWithIllegalArgumentException() {
+ VespaModelFactory modelFactory = new VespaModelFactory(new NullConfigModelRegistry());
+ modelFactory.createAndValidateModel(new MockModelContext(createApplicationPackageThatFailsWhenValidating()), false);
+ }
+
+ // Uses a MockApplicationPackage that throws throws UnsupportedOperationException (rethrown as RuntimeException) when validating
+ @Test(expected = RuntimeException.class)
+ public void testThatFactoryModelValidationFails() {
+ VespaModelFactory modelFactory = new VespaModelFactory(new NullConfigModelRegistry());
+ modelFactory.createAndValidateModel(testModelContext, false);
+ }
+
+ @Test
+ public void testThatFactoryModelValidationCanBeIgnored() {
+ VespaModelFactory modelFactory = new VespaModelFactory(new NullConfigModelRegistry());
+ ModelCreateResult createResult = modelFactory.createAndValidateModel(
+ new MockModelContext(createApplicationPackageThatFailsWhenValidating()),
+ true);
+ assertNotNull(createResult.getModel());
+ assertNotNull(createResult.getConfigChangeActions());
+ assertTrue(createResult.getConfigChangeActions().isEmpty());
+ }
+
+ @Test
+ public void hostedVespaRoutingApplicationAllocatesNodesWithHostsXml() {
+ String hostName = "test-host-name";
+ String routingClusterName = "routing-cluster";
+
+ String hosts =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<hosts>\n" +
+ " <host name='" + hostName + "'>\n" +
+ " <alias>proxy1</alias>\n" +
+ " </host>\n" +
+ "</hosts>";
+
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services version='1.0' xmlns:deploy='vespa'>\n" +
+ " <admin version='2.0'>\n" +
+ " <adminserver hostalias='proxy1' />\n" +
+ " </admin>" +
+ " <jdisc id='" + routingClusterName + "' version='1.0'>\n" +
+ " <nodes>\n" +
+ " <node hostalias='proxy1' />\n" +
+ " </nodes>\n" +
+ " </jdisc>\n" +
+ "</services>";
+
+ HostProvisioner provisionerToOverride =
+ mock(HostProvisioner.class, new ThrowsExceptionClass(UnsupportedOperationException.class));
+
+ ModelContext modelContext = new MockModelContext() {
+ @Override
+ public ApplicationPackage applicationPackage() {
+ return new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).build();
+ }
+
+ @Override
+ public Optional<HostProvisioner> hostProvisioner() {
+ return Optional.of(provisionerToOverride);
+ }
+
+ @Override
+ public Properties properties() {
+ return new Properties() {
+ @Override
+ public boolean multitenant() {
+ return true;
+ }
+
+ @Override
+ public boolean hostedVespa() {
+ return true;
+ }
+
+ @Override
+ public Zone zone() {
+ return Zone.defaultZone();
+ }
+
+ @Override
+ public Set<Rotation> rotations() {
+ return new HashSet<>();
+ }
+
+ @Override
+ public ApplicationId applicationId() {
+ return ApplicationId.HOSTED_ZONE_APPLICATION_ID;
+ }
+
+ @Override
+ public List<ConfigServerSpec> configServerSpecs() {
+ return Collections.emptyList();
+ }
+ };
+ }
+ };
+
+ Model model = new VespaModelFactory(new NullConfigModelRegistry()).createModel(modelContext);
+
+ List<HostInfo> allocatedHosts = new ArrayList<>(model.getHosts());
+ assertThat(allocatedHosts.size(), is(1));
+ HostInfo hostInfo = allocatedHosts.get(0);
+
+ assertThat(hostInfo.getHostname(), is(hostName));
+
+ assertTrue("Routing service should run on host " + hostName,
+ hostInfo.getServices().stream()
+ .map(ServiceInfo::getConfigId)
+ .anyMatch(configId -> configId.contains(routingClusterName)));
+ }
+
+ @Test
+ public void hostedVespaZoneApplicationAllocatesNodesWithHostsXml() {
+ String hostName = "test-host-name";
+ String routingClusterName = "routing-cluster";
+
+ String hosts =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<hosts>\n" +
+ " <host name='" + hostName + "'>\n" +
+ " <alias>proxy1</alias>\n" +
+ " </host>\n" +
+ "</hosts>";
+
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services version='1.0' xmlns:deploy='vespa'>\n" +
+ " <admin version='2.0'>\n" +
+ " <adminserver hostalias='proxy1' />\n" +
+ " </admin>" +
+ " <jdisc id='" + routingClusterName + "' version='1.0'>\n" +
+ " <nodes>\n" +
+ " <node hostalias='proxy1' />\n" +
+ " </nodes>\n" +
+ " </jdisc>\n" +
+ "</services>";
+
+ HostProvisioner provisionerToOverride =
+ mock(HostProvisioner.class, new ThrowsExceptionClass(UnsupportedOperationException.class));
+
+ ModelContext modelContext = new MockModelContext() {
+ @Override
+ public ApplicationPackage applicationPackage() {
+ return new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).build();
+ }
+
+ @Override
+ public Optional<HostProvisioner> hostProvisioner() {
+ return Optional.of(provisionerToOverride);
+ }
+
+ @Override
+ public Properties properties() {
+ return new Properties() {
+ @Override
+ public boolean multitenant() {
+ return true;
+ }
+
+ @Override
+ public boolean hostedVespa() {
+ return true;
+ }
+
+ @Override
+ public Zone zone() {
+ return Zone.defaultZone();
+ }
+
+ @Override
+ public Set<Rotation> rotations() {
+ return new HashSet<>();
+ }
+
+ @Override
+ public ApplicationId applicationId() {
+ return ApplicationId.HOSTED_ZONE_APPLICATION_ID;
+ }
+
+ @Override
+ public List<ConfigServerSpec> configServerSpecs() {
+ return Collections.emptyList();
+ }
+ };
+ }
+ };
+
+ Model model = new VespaModelFactory(new NullConfigModelRegistry()).createModel(modelContext);
+
+ List<HostInfo> allocatedHosts = new ArrayList<>(model.getHosts());
+ assertThat(allocatedHosts.size(), is(1));
+ HostInfo hostInfo = allocatedHosts.get(0);
+
+ assertThat(hostInfo.getHostname(), is(hostName));
+
+ assertTrue("Routing service should run on host " + hostName,
+ hostInfo.getServices().stream()
+ .map(ServiceInfo::getConfigId)
+ .anyMatch(configId -> configId.contains(routingClusterName)));
+ }
+
+ ApplicationPackage createApplicationPackageThatFailsWhenValidating() {
+ return new MockApplicationPackage.Builder().withEmptyHosts().withEmptyServices().failOnValidateXml().build();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/AdminTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/AdminTestCase.java
new file mode 100644
index 00000000000..a8554c54867
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/AdminTestCase.java
@@ -0,0 +1,256 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.cloud.config.SlobroksConfig;
+import com.yahoo.cloud.config.SlobroksConfig.Slobrok;
+import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.model.ApplicationConfigProducerRoot;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.model.test.TestRoot;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.StatisticsConfig;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.StatisticsComponent;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+
+/**
+ * @author gjoranv
+ */
+public class AdminTestCase {
+
+ protected static final String TESTDIR = "src/test/cfg/admin/";
+
+ protected VespaModel getVespaModel(String configPath) {
+ return new VespaModelCreatorWithFilePkg(configPath).create();
+ }
+
+ /**
+ * Test that version 2.0 of adminconfig works as expected.
+ */
+ @Test
+ public void testAdmin20() throws Exception {
+ VespaModel vespaModel = getVespaModel(TESTDIR + "adminconfig20");
+
+ // Verify that the admin plugin has been loaded (always loads routing).
+ assertThat(vespaModel.configModelRepo().asMap().size(), is(2));
+
+ ApplicationConfigProducerRoot root = vespaModel.getVespa();
+ assertNotNull(root);
+
+ // Verify configIds
+ Set<String> configIds = vespaModel.getConfigIds();
+ String localhost = HostName.getLocalhost();
+ String localhostConfigId = "hosts/" + localhost;
+ assertTrue(configIds.contains(localhostConfigId));
+ assertTrue(configIds.contains("admin/logserver"));
+ assertTrue(configIds.contains("admin/configservers/configserver.0"));
+ assertTrue(configIds.contains("admin/slobrok.0"));
+ assertTrue(configIds.contains("admin/slobrok.1"));
+ assertFalse(configIds.contains("admin/slobrok.2"));
+ assertTrue(configIds.contains("admin"));
+
+ // Confirm 2 slobroks in config
+ SlobroksConfig.Builder sb = new SlobroksConfig.Builder();
+ vespaModel.getConfig(sb, "admin/slobrok.0");
+ SlobroksConfig sc = new SlobroksConfig(sb);
+ assertEquals(sc.slobrok().size(), 2);
+ boolean localHostOK = false;
+ for (Slobrok s : sc.slobrok()) {
+ if (s.connectionspec().matches(".*" + localhost + ".*")) localHostOK = true;
+ }
+ assertTrue(localHostOK);
+
+ LogdConfig.Builder lb = new LogdConfig.Builder();
+ vespaModel.getConfig(lb, "admin/slobrok.0");
+ LogdConfig lc = new LogdConfig(lb);
+ assertEquals(lc.logserver().host(), localhost);
+
+ // Verify services in the sentinel config
+ SentinelConfig.Builder b = new SentinelConfig.Builder();
+ vespaModel.getConfig(b, localhostConfigId);
+ SentinelConfig sentinelConfig = new SentinelConfig(b);
+ assertThat(sentinelConfig.service().size(), is(5));
+ assertThat(sentinelConfig.service(0).name(), is("logserver"));
+ assertThat(sentinelConfig.service(1).name(), is("slobrok"));
+ assertThat(sentinelConfig.service(2).name(), is("slobrok2"));
+ assertThat(sentinelConfig.service(3).name(), is("logd"));
+ assertThat(sentinelConfig.service(4).name(), is("filedistributorservice"));
+ }
+
+ /**
+ * Test that a very simple config with only adminserver tag creates
+ * adminserver, logserver, configserver and slobroks
+ */
+ @Test
+ public void testOnlyAdminserver() throws Exception {
+ VespaModel vespaModel = getVespaModel(TESTDIR + "simpleadminconfig20");
+
+ // Verify that the admin plugin has been loaded (always loads routing).
+ assertThat(vespaModel.configModelRepo().asMap().size(), is(2));
+
+ ApplicationConfigProducerRoot root = vespaModel.getVespa();
+ assertNotNull(root);
+
+ // Verify configIds
+ Set<String> configIds = vespaModel.getConfigIds();
+ String localhost = HostName.getLocalhost();
+ String localhostConfigId = "hosts/" + localhost;
+ assertTrue(configIds.contains(localhostConfigId));
+ assertTrue(configIds.contains("admin/logserver"));
+ assertTrue(configIds.contains("admin/configservers/configserver.0"));
+ assertTrue(configIds.contains("admin/slobrok.0"));
+ assertFalse(configIds.contains("admin/slobrok.1"));
+
+ // Verify services in the sentinel config
+ SentinelConfig.Builder b = new SentinelConfig.Builder();
+ vespaModel.getConfig(b, localhostConfigId);
+ SentinelConfig sentinelConfig = new SentinelConfig(b);
+ assertThat(sentinelConfig.service().size(), is(4));
+ assertThat(sentinelConfig.service(0).name(), is("logserver"));
+ assertThat(sentinelConfig.service(1).name(), is("slobrok"));
+ assertThat(sentinelConfig.service(2).name(), is("logd"));
+ assertThat(sentinelConfig.service(3).name(), is("filedistributorservice"));
+ assertThat(sentinelConfig.service(0).affinity().cpuSocket(), is(-1));
+ assertTrue(sentinelConfig.service(0).preShutdownCommand().isEmpty());
+
+ // Confirm slobrok config
+ SlobroksConfig.Builder sb = new SlobroksConfig.Builder();
+ vespaModel.getConfig(sb, "admin");
+ SlobroksConfig sc = new SlobroksConfig(sb);
+ assertEquals(sc.slobrok().size(), 1);
+ assertTrue(sc.slobrok().get(0).connectionspec().matches(".*" + localhost + ".*"));
+ }
+
+ @Test
+ public void testTenantAndAppInSentinelConfig() {
+ DeployState state = new DeployState.Builder().properties(
+ new DeployProperties.Builder().
+ zone(new Zone(Environment.dev, RegionName.from("baz"))).
+ applicationId(new ApplicationId.Builder().
+ tenant("quux").
+ applicationName("foo").instanceName("bim").build()).build()).build();
+ TestRoot root = new TestDriver().buildModel(state);
+ String localhost = HostName.getLocalhost();
+ SentinelConfig config = root.getConfig(SentinelConfig.class, "hosts/" + localhost);
+ assertThat(config.application().tenant(), is("quux"));
+ assertThat(config.application().name(), is("foo"));
+ assertThat(config.application().environment(), is("dev"));
+ assertThat(config.application().region(), is("baz"));
+ assertThat(config.application().instance(), is("bim"));
+ }
+
+ @Test
+ public void testMultipleConfigServers() throws Exception {
+ VespaModel vespaModel = getVespaModel(TESTDIR + "multipleconfigservers");
+
+ // Verify that the admin plugin has been loaded (always loads routing).
+ assertThat(vespaModel.configModelRepo().asMap().size(), is(2));
+ ApplicationConfigProducerRoot root = vespaModel.getVespa();
+ assertNotNull(root);
+
+ Admin admin = vespaModel.getAdmin();
+ assertNotNull(admin);
+
+ // Verify configIds
+ Set<String> configIds = vespaModel.getConfigIds();
+ String localhost = HostName.getLocalhost();
+ String localhostConfigId = "hosts/" + localhost;
+ assertTrue(configIds.contains(localhostConfigId));
+ assertTrue(configIds.contains("admin/logserver"));
+ assertTrue(configIds.contains("admin/configservers/configserver.0"));
+ assertTrue(configIds.contains("admin/configservers/configserver.1"));
+
+ assertThat(admin.getConfigservers().size(), is(2));
+
+ // Default configserver is the first one in the list and should have the default ports too
+ Configserver server1 = admin.getConfigservers().get(0);
+ assertEquals(admin.getConfigserver(), server1);
+ assertThat(server1.getPortCount(), is(2));
+ assertThat(server1.getRelativePort(0), is(19070));
+ assertThat(server1.getRelativePort(1), is(19071));
+
+
+ // Second configserver should be on second host but have the same port number
+ Configserver server2 = admin.getConfigservers().get(1);
+
+ assertNotSame(server1, server2);
+ assertNotSame(server1.getHostName(), server2.getHostName());
+
+ assertThat(server2.getPortCount(), is(2));
+ assertThat(server2.getRelativePort(0), is(19070));
+ assertThat(server2.getRelativePort(1), is(19071));
+ }
+
+ @Test
+ public void testContainerMetricsSnapshotInterval() throws Exception {
+ VespaModel vespaModel = getVespaModel(TESTDIR + "metricconfig");
+
+ ContainerCluster docprocCluster = vespaModel.getContainerClusters().get("cluster.music.indexing");
+ HealthMonitorConfig.Builder builder = new HealthMonitorConfig.Builder();
+ docprocCluster.getConfig(builder);
+ HealthMonitorConfig docprocConfig = new HealthMonitorConfig(builder);
+ assertEquals(60, (int) docprocConfig.snapshot_interval());
+
+ ContainerCluster qrCluster = vespaModel.getContainerClusters().get("container");
+ builder = new HealthMonitorConfig.Builder();
+ qrCluster.getConfig(builder);
+ HealthMonitorConfig qrClusterConfig = new HealthMonitorConfig(builder);
+ assertEquals(60, (int) qrClusterConfig.snapshot_interval());
+
+ StatisticsComponent stat = null;
+ for (Component component : qrCluster.getAllComponents()) {
+ System.out.println(component.getClassId().getName());
+ if (component.getClassId().getName().contains("com.yahoo.statistics.StatisticsImpl")) {
+ stat = (StatisticsComponent) component;
+ break;
+ }
+ }
+ assertNotNull(stat);
+ StatisticsConfig.Builder sb = new StatisticsConfig.Builder();
+ stat.getConfig(sb);
+ StatisticsConfig sc = new StatisticsConfig(sb);
+ assertEquals(60, (int) sc.collectionintervalsec());
+ assertEquals(60, (int) sc.loggingintervalsec());
+ }
+
+ @Test
+ public void testStatisticsConfig() {
+ StatisticsComponent stat = new StatisticsComponent();
+ StatisticsConfig.Builder sb = new StatisticsConfig.Builder();
+ stat.getConfig(sb);
+ StatisticsConfig sc = new StatisticsConfig(sb);
+ assertEquals(sc.collectionintervalsec(), 300, 0.1);
+ assertEquals(sc.loggingintervalsec(), 300, 0.1);
+ assertEquals(sc.values(0).operations(0).name(), StatisticsConfig.Values.Operations.Name.REGULAR);
+ assertEquals(sc.values(0).operations(0).arguments(0).key(), "limits");
+ assertEquals(sc.values(0).operations(0).arguments(0).value(), "25,50,100,500");
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java
new file mode 100644
index 00000000000..200e51ef924
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java
@@ -0,0 +1,440 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.yahoo.cloud.config.ZookeeperServerConfig;
+import com.yahoo.cloud.config.ZookeepersConfig;
+import com.yahoo.config.model.application.provider.SimpleApplicationValidator;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.model.test.TestRoot;
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Collection;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ * Test for creating cluster controllers under the admin tag.
+ */
+public class ClusterControllerTestCase extends DomBuilderTest {
+
+ private List<String> sds;
+
+ @Before
+ public void setup() {
+ sds = ApplicationPackageUtils.generateSearchDefinitions("type1", "type2");
+ }
+
+ @Test
+ public void testSingleCluster() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"configserver\" />\n" +
+ " <cluster-controllers>\n" +
+ " <cluster-controller hostalias=\"configserver\">" +
+ " </cluster-controller>" +
+ " <cluster-controller hostalias=\"configserver\"/>" +
+ " <cluster-controller hostalias=\"configserver\"/>" +
+ " </cluster-controllers>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ " <tuning>" +
+ " <cluster-controller>\n" +
+ " <init-progress-time>34567s</init-progress-time>" +
+ " <transition-time>4000ms</transition-time>" +
+ " <stable-state-period>1h</stable-state-period>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+
+ VespaModel model = createVespaModel(xml);
+ assertTrue(model.getService("admin/cluster-controllers/0").isPresent());
+
+ assertTrue(existsHostsWithClusterControllerConfigId(model));
+ assertGroupSize(model, "admin/cluster-controllers/0/components/clustercontroller-bar-configurer", 1);
+ for (int i = 0; i < 3; ++i) {
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ model.getConfig(builder, "admin/cluster-controllers/" + i + "/components/clustercontroller-bar-configurer");
+
+ FleetcontrollerConfig cfg = new FleetcontrollerConfig(builder);
+ assertThat(cfg.index(), is(i));
+ assertThat(cfg.fleet_controller_count(), is(3));
+ assertThat(cfg.init_progress_time(), is(34567000));
+ assertThat(cfg.storage_transition_time(), is(4000));
+ assertThat(cfg.stable_state_time_period(), is(3600000));
+ }
+ }
+
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSeparateHostsRequired() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"mockhost\" />\n" +
+ " <cluster-controllers standalone-zookeeper=\"true\">\n" +
+ " <cluster-controller hostalias=\"mockhost\"/>" +
+ " <cluster-controller hostalias=\"mockhost\"/>" +
+ " <cluster-controller hostalias=\"mockhost\"/>" +
+ " </cluster-controllers>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='mockhost' distribution-key='0' />" +
+ " </group>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+ TestDriver driver = new TestDriver();
+ driver.buildModel(xml);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSeparateHostsFromConfigServerRequired() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"mockhost\" />\n" +
+ " <configservers>\n" +
+ " <configserver hostalias=\"mockhost\" />" +
+ " </configservers>" +
+ " <cluster-controllers standalone-zookeeper=\"true\">\n" +
+ " <cluster-controller hostalias=\"mockhost\"/>" +
+ " </cluster-controllers>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='mockhost' distribution-key='0' />" +
+ " </group>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+ TestDriver driver = new TestDriver();
+ driver.buildModel(xml);
+ }
+
+ @Test
+ public void testStandaloneZooKeeper() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"node1\" />\n" +
+ " <cluster-controllers standalone-zookeeper=\"true\">\n" +
+ " <cluster-controller hostalias=\"node2\"/>" +
+ " <cluster-controller hostalias=\"node3\"/>" +
+ " <cluster-controller hostalias=\"node4\"/>" +
+ " </cluster-controllers>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node1' distribution-key='0' />" +
+ " </group>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+ String hosts = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<hosts>\n" +
+ " <host name=\"localhost\">\n" +
+ " <alias>node1</alias>\n" +
+ " </host>\n" +
+ " <host name=\"my.host1.com\">\n" +
+ " <alias>node2</alias>\n" +
+ " </host>\n" +
+ " <host name=\"my.host2.com\">\n" +
+ " <alias>node3</alias>\n" +
+ " </host>\n" +
+ " <host name=\"my.host3.com\">\n" +
+ " <alias>node4</alias>\n" +
+ " </host>\n" +
+ "</hosts>";
+ TestDriver driver = new TestDriver();
+ TestRoot root = driver.buildModel(xml, hosts);
+ assertZookeepersConfig(root);
+ assertZookeeperServerConfig(root, 0);
+ assertZookeeperServerConfig(root, 1);
+ assertZookeeperServerConfig(root, 2);
+ }
+
+ private void assertZookeepersConfig(TestRoot root) {
+ ZookeepersConfig.Builder builder = new ZookeepersConfig.Builder();
+ root.getConfig(builder, "admin/standalone");
+ ZookeepersConfig config = new ZookeepersConfig(builder);
+ assertThat(config.zookeeperserverlist().split(",").length, is(3));
+ }
+
+ private void assertZookeeperServerConfig(TestRoot root, int id) {
+ ZookeeperServerConfig.Builder builder = new ZookeeperServerConfig.Builder();
+ root.getConfig(builder, "admin/standalone/cluster-controllers/" + id);
+ ZookeeperServerConfig config = new ZookeeperServerConfig(builder);
+ assertThat(config.server().size(), is(3));
+ assertThat(config.myid(), is(id));
+ Collection<Integer> serverIds = Collections2.transform(config.server(), new Function<ZookeeperServerConfig.Server, Integer>() {
+ @Override
+ public Integer apply(ZookeeperServerConfig.Server server) {
+ return server.id();
+ }
+ });
+ assertTrue(serverIds.contains(id));
+ }
+
+
+ @Test
+ public void testUnconfigured() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"configserver\" />\n" +
+ " <logserver hostalias=\"logserver\" />\n" +
+ " <slobroks>\n" +
+ " <slobrok hostalias=\"configserver\" />\n" +
+ " <slobrok hostalias=\"logserver\" />\n" +
+ " </slobroks>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ " <tuning>" +
+ " <cluster-controller>\n" +
+ " <init-progress-time>34567</init-progress-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+ VespaModel model = createVespaModel(xml);
+ assertTrue(model.getService("admin/cluster-controllers/0").isPresent());
+
+ assertTrue(existsHostsWithClusterControllerConfigId(model));
+ assertGroupSize(model, "admin/cluster-controllers/0/components/clustercontroller-bar-configurer", 1);
+ assertThat(model.getAdmin().getClusterControllers().getContainers().size(), is(1));
+
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ model.getConfig(builder, "admin/cluster-controllers/0/components/clustercontroller-bar-configurer");
+
+ FleetcontrollerConfig cfg = new FleetcontrollerConfig(builder);
+ assertThat(cfg.index(), is(0));
+ assertThat(cfg.fleet_controller_count(), is(1));
+ assertThat(cfg.init_progress_time(), is(34567000));
+ }
+
+ private boolean existsHostsWithClusterControllerConfigId(VespaModel model) {
+ boolean found = false;
+ for (HostResource h : model.getHostSystem().getHosts()) {
+ for (Service s : h.getServices()) {
+ if (s.getConfigId().equals("admin/cluster-controllers/0")) {
+ found = true;
+ }
+ }
+ }
+ return found;
+ }
+
+ @Test
+ public void testUnconfiguredMultiple() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"configserver\" />\n" +
+ " <configservers>\n" +
+ " <configserver hostalias=\"node0\"/>" +
+ " <configserver hostalias=\"node1\"/>" +
+ " <configserver hostalias=\"node2\"/>" +
+ " </configservers>\n" +
+ " <slobroks>\n" +
+ " <slobrok hostalias=\"configserver\" />\n" +
+ " <slobrok hostalias=\"logserver\" />\n" +
+ " </slobroks>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ " <tuning>\n" +
+ " <cluster-controller>" +
+ " <init-progress-time>34567</init-progress-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+ VespaModel model = createVespaModel(xml);
+
+ assertThat(model.getAdmin().getClusterControllers().getContainers().size(), is(3));
+ assertGroupSize(model, "admin/cluster-controllers/0/components/clustercontroller-bar-configurer", 1);
+ assertGroupSize(model, "admin/cluster-controllers/1/components/clustercontroller-bar-configurer", 1);
+ assertGroupSize(model, "admin/cluster-controllers/2/components/clustercontroller-bar-configurer", 1);
+ }
+
+ @Test
+ public void testUnconfiguredNoTuning() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"configserver\" />\n" +
+ " <logserver hostalias=\"logserver\" />\n" +
+ " <slobroks>\n" +
+ " <slobrok hostalias=\"configserver\" />\n" +
+ " <slobrok hostalias=\"logserver\" />\n" +
+ " </slobroks>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+ VespaModel model = createVespaModel(xml);
+ assertTrue(model.getService("admin/cluster-controllers/0").isPresent());
+
+ assertTrue(existsHostsWithClusterControllerConfigId(model));
+ assertGroupSize(model, "admin/cluster-controllers/0/components/clustercontroller-bar-configurer", 1);
+ assertThat(model.getAdmin().getClusterControllers().getContainers().size(), is(1));
+
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ model.getConfig(builder, "admin/cluster-controllers/0/components/clustercontroller-bar-configurer");
+
+ FleetcontrollerConfig cfg = new FleetcontrollerConfig(builder);
+ assertThat(cfg.index(), is(0));
+ }
+
+ @Test
+ public void testUnconfiguredNoContent() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"configserver\" />\n" +
+ " </admin>\n" +
+ " <container version=\"1.0\">\n" +
+ " <nodes>" +
+ " <node hostalias=\"node1\"/>\n" +
+ " </nodes>\n" +
+ " </container>\n" +
+ "</services>";
+
+ VespaModel model = createVespaModel(xml);
+ assertFalse(model.getService("admin/cluster-controllers/0").isPresent());
+
+ assertFalse(existsHostsWithClusterControllerConfigId(model));
+ assertNull(model.getAdmin().getClusterControllers());
+ }
+
+ @Test
+ public void testUsingOldStyle() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"configserver\" />\n" +
+ " <logserver hostalias=\"logserver\" />\n" +
+ " <slobroks>\n" +
+ " <slobrok hostalias=\"configserver\" />\n" +
+ " <slobrok hostalias=\"logserver\" />\n" +
+ " </slobroks>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ " <tuning>\n" +
+ " <cluster-controller>" +
+ " <init-progress-time>34567</init-progress-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+ VespaModel model = createVespaModel(xml);
+ assertTrue(model.getService("admin/cluster-controllers/0").isPresent());
+
+ assertTrue(existsHostsWithClusterControllerConfigId(model));
+ {
+ //StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ //try {
+ // model.getConfig(builder, "admin/cluster-controllers/0/components/bar-configurer");
+ // fail("Invalid config id didn't fail.");
+ //} catch (UnknownConfigIdException e) {
+ // assertTrue(e.getMessage().matches(".*Invalid config id.*"));
+ //}
+ }
+ }
+
+ private void assertGroupSize(VespaModel model, String configId, int size) {
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ model.getConfig(builder, configId);
+ StorDistributionConfig cfg = new StorDistributionConfig(builder);
+ assertThat(cfg.group().size(), is(size));
+ }
+
+ private VespaModel createVespaModel(String servicesXml) throws IOException, SAXException {
+ VespaModel model = new VespaModel(new MockApplicationPackage.Builder()
+ .withServices(servicesXml)
+ .withSearchDefinitions(sds)
+ .build());
+ SimpleApplicationValidator.checkServices(new StringReader(servicesXml));
+ return model;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java
new file mode 100644
index 00000000000..b4e366c1609
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/DedicatedAdminV4Test.java
@@ -0,0 +1,76 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.admin;
+
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.provision.Hosts;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @author bratseth
+ */
+public class DedicatedAdminV4Test {
+
+ private static final String services =
+ "<services>" +
+ " <admin version='4.0'>" +
+ " <slobroks><nodes count='2' dedicated='true'/></slobroks>" +
+ " <logservers><nodes count='1' dedicated='true'/></logservers>" +
+ " </admin>" +
+ "</services>";
+
+ @Test
+ public void testModelBuilding() throws IOException, SAXException {
+ String hosts = "<hosts>"
+ + " <host name=\"myhost0\">"
+ + " <alias>node0</alias>"
+ + " </host>"
+ + " <host name=\"myhost1\">"
+ + " <alias>node1</alias>"
+ + " </host>"
+ + " <host name=\"myhost2\">"
+ + " <alias>node2</alias>"
+ + " </host>"
+ + "</hosts>";
+ ApplicationPackage app = new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).build();
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder().applicationPackage(app).modelHostProvisioner(new InMemoryProvisioner(Hosts.getHosts(app.getHosts()), true)).build());
+ assertEquals(3, model.getHosts().size());
+
+ Set<String> serviceNames0 = serviceNames(model.getConfig(SentinelConfig.class, "hosts/myhost0"));
+ assertEquals(3, serviceNames0.size());
+ assertTrue(serviceNames0.contains("slobrok"));
+ assertTrue(serviceNames0.contains("logd"));
+ assertTrue(serviceNames0.contains("filedistributorservice"));
+
+ Set<String> serviceNames1 = serviceNames(model.getConfig(SentinelConfig.class, "hosts/myhost1"));
+ assertEquals(3, serviceNames1.size());
+ assertTrue(serviceNames1.contains("slobrok"));
+ assertTrue(serviceNames1.contains("logd"));
+ assertTrue(serviceNames1.contains("filedistributorservice"));
+
+ Set<String> serviceNames2 = serviceNames(model.getConfig(SentinelConfig.class, "hosts/myhost2"));
+ assertEquals(3, serviceNames2.size());
+ assertTrue(serviceNames2.contains("logserver"));
+ assertTrue(serviceNames2.contains("logd"));
+ assertTrue(serviceNames2.contains("filedistributorservice"));
+ }
+
+ private Set<String> serviceNames(SentinelConfig config) {
+ return config.service().stream().map(SentinelConfig.Service::name).collect(Collectors.toSet());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ComponentValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ComponentValidatorTest.java
new file mode 100644
index 00000000000..3efd7e171d3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ComponentValidatorTest.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import org.junit.Test;
+
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.jar.JarFile;
+import java.util.logging.Level;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public class ComponentValidatorTest {
+ private static final String JARS_DIR = "src/test/cfg/application/validation/testjars/";
+
+ @Test
+ public void basicComponentValidation() throws Exception {
+ // Valid jar file
+ JarFile ok = new JarFile(new File(JARS_DIR + "ok.jar"));
+ ComponentValidator componentValidator = new ComponentValidator(ok);
+ componentValidator.validateAll(new BaseDeployLogger());
+
+ // No manifest
+ validateWithException("nomanifest.jar", "Non-existing or invalid manifest in " + JARS_DIR + "nomanifest.jar");
+ }
+
+ private void validateWithException(String jarName, String exceptionMessage) throws IOException {
+ try {
+ JarFile jarFile = new JarFile(JARS_DIR + jarName);
+ ComponentValidator componentValidator = new ComponentValidator(jarFile);
+ componentValidator.validateAll(new BaseDeployLogger());
+ assert (false);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is(exceptionMessage));
+ }
+ }
+
+ @Test
+ public void require_that_deploying_snapshot_bundle_gives_warning() throws IOException {
+ final StringBuffer buffer = new StringBuffer();
+
+ DeployLogger logger = new DeployLogger() {
+ @Override
+ public void log(Level level, String message) {
+ buffer.append(message).append('\n');
+ }
+ };
+
+ new ComponentValidator(new JarFile(JARS_DIR + "snapshot_bundle.jar")).validateAll(logger);
+ assertThat(buffer.toString(), containsString("Deploying snapshot bundle"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidatorTest.java
new file mode 100644
index 00000000000..bfd25b8587c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/DeploymentFileValidatorTest.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.model.VespaModel;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Harald Musum</a>
+ */
+public class DeploymentFileValidatorTest {
+
+ @Test
+ public void testDeploymentWithNonExistentGlobalId() throws IOException, SAXException {
+ final String simpleHosts = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<hosts> " +
+ "<host name=\"localhost\">" +
+ "<alias>node0</alias>" +
+ "</host>" +
+ "</hosts>";
+
+ final String services = "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0' />" +
+ " </admin>" +
+ " <jdisc id='default' version='1.0'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='node0'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ "</services>";
+
+ final String deploymentInfo = "<?xml version='1.0' encoding='UTF-8'?>" +
+ "<deployment version='1.0'>" +
+ " <test />" +
+ " <prod global-service-id='non-existing'>" +
+ " <region active='true'>us-east</region>" +
+ " </prod>" +
+ "</deployment>";
+
+ ApplicationPackage app = new MockApplicationPackage.Builder()
+ .withHosts(simpleHosts)
+ .withServices(services)
+ .withDeploymentInfo(deploymentInfo)
+ .build();
+ DeployState.Builder builder = new DeployState.Builder().applicationPackage(app);
+ try {
+ final DeployState deployState = builder.build();
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+ new DeploymentFileValidator().validate(model, deployState);
+ fail("Did not get expected exception");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("specified in deployment.xml does not match any container cluster id"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexesTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexesTest.java
new file mode 100644
index 00000000000..a9532fb52b3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/NoPrefixForIndexesTest.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Harald Musum</a>
+ */
+public class NoPrefixForIndexesTest {
+
+ @Test
+ public void requireThatPrefixIsSupported() {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/prefix/").create();
+ }
+
+ @Test
+ public void requireThatPrefixIsSupportedForStreaming() {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/prefix_streaming/").create();
+ }
+
+ @Test
+ public void requireThatPrefixIsIllegalForIndexField() {
+ try {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/prefix_index/").create();
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("For search 'simple', field 'artist': match/index:prefix is not supported for indexes.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatPrefixIsIllegalForMixedAttributeAndIndexField() {
+ try {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/prefix_index_and_attribute/").create();
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("For search 'simple', field 'artist': match/index:prefix is not supported for indexes.", e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidatorTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidatorTestCase.java
new file mode 100644
index 00000000000..8c1a288c46d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/SearchDataTypeValidatorTestCase.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SearchDataTypeValidatorTestCase {
+
+ @Test
+ public void requireThatSupportedTypesAreValidated() {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/search_alltypes/").create();
+ }
+
+ @Test
+ public void requireThatStructsAreLegalInSearchClusters() {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/search_struct/").create();
+ }
+
+ @Test
+ public void requireThatEmptyContentFieldIsLegalInSearchClusters() {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/search_empty_content/").create();
+ }
+
+ @Test
+ public void requireThatIndexingMapsInNonStreamingClusterIsIllegal() {
+ try {
+ new VespaModelCreatorWithFilePkg("src/test/cfg/application/validation/index_struct/").create();
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Field type 'Map<string,string>' cannot be indexed for search clusters (field 'baz' in definition " +
+ "'simple' for cluster 'content').", e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationOverrideTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationOverrideTest.java
new file mode 100644
index 00000000000..3354e9320fc
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationOverrideTest.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.vespa.model.application.validation.xml.ValidationOverridesXMLReader;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Optional;
+
+import static org.junit.Assert.fail;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bratseth
+ */
+public class ValidationOverrideTest {
+
+ @Test
+ public void testValidationOverridesInIsolation() throws IOException, SAXException {
+ String validationOverrides =
+ "<validation-overrides>" +
+ " <allow until='2000-01-01'>indexing-change</allow>" +
+ " <allow until='2000-01-03' comment='any text'>indexing-mode-change</allow>" +
+ "</validation-overrides>";
+
+ {
+
+ ValidationOverrides overrides = new ValidationOverridesXMLReader().read(Optional.of(new StringReader(validationOverrides)),
+ at("2000-01-01T23:59:00"));
+ assertOverridden("indexing-change", overrides);
+ assertOverridden("indexing-mode-change", overrides);
+ assertNotOverridden("field-type-change", overrides);
+ }
+
+ {
+ ValidationOverrides overrides = new ValidationOverridesXMLReader().read(Optional.of(new StringReader(validationOverrides)),
+ at("2000-01-02T00:00:00"));
+ assertNotOverridden("indexing-change", overrides);
+ assertOverridden("indexing-mode-change", overrides);
+ assertNotOverridden("field-type-change", overrides);
+ }
+
+ {
+ ValidationOverrides overrides = new ValidationOverridesXMLReader().read(Optional.of(new StringReader(validationOverrides)),
+ at("2000-01-04T00:00:00"));
+ assertNotOverridden("indexing-change", overrides);
+ assertNotOverridden("indexing-mode-change", overrides);
+ assertNotOverridden("field-type-change", overrides);
+ }
+
+ }
+
+ @Test
+ public void testInvalidOverridePeriod() throws IOException, SAXException {
+ String validationOverrides =
+ "<validation-overrides>" +
+ " <allow until='2000-02-02'>indexing-change</allow>" +
+ "</validation-overrides>";
+
+ try {
+ new ValidationOverridesXMLReader().read(Optional.of(new StringReader(validationOverrides)),
+ at("2000-01-01T23:59:00"));
+ fail("Expected validation interval override validation validation failure");
+ }
+ catch (IllegalArgumentException e) {
+ assertEquals("validation-overrides is invalid", e.getMessage());
+ assertEquals("allow 'indexing-change' until 2000-02-03T00:00:00Z is too far in the future: Max 30 days is allowed",
+ e.getCause().getMessage());
+ }
+ }
+
+ private Instant at(String utcIsoTime) {
+ return LocalDateTime.parse(utcIsoTime, DateTimeFormatter.ISO_DATE_TIME).atZone(ZoneOffset.UTC).toInstant();
+ }
+
+ private void assertOverridden(String validationId, ValidationOverrides overrides) {
+ overrides.invalid(ValidationId.from(validationId).get(), "message"); // should not throw exception
+ }
+
+ private void assertNotOverridden(String validationId, ValidationOverrides overrides) {
+ try {
+ overrides.invalid(ValidationId.from(validationId).get(), "message");
+ fail("Expected '" + validationId + "' to not be overridden");
+ }
+ catch (ValidationOverrides.ValidationException expected) {
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java
new file mode 100644
index 00000000000..75f4eb3e15d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/ValidationTester.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * @author bratseth
+ */
+public class ValidationTester {
+
+ private final int nodeCount;
+
+ /** Creates a validation tester with 1 node available */
+ public ValidationTester() {
+ this(1);
+ }
+
+ /** Creates a validation tester with a number of nodes available */
+ public ValidationTester(int nodeCount) {
+ this.nodeCount = nodeCount;
+ }
+
+ /**
+ * Deploys an application
+ *
+ * @param previousModel the previous model, or null if no previous
+ * @param services the services file content
+ * @param validationOverrides the validation overrides file content, or null if none
+ * @return the new model and any change actions
+ */
+ public Pair<VespaModel, List<ConfigChangeAction>> deploy(VespaModel previousModel, String services, String validationOverrides) {
+ Instant now = LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant();
+ ApplicationPackage newApp = new MockApplicationPackage.Builder()
+ .withServices(services)
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .withValidationOverrides(validationOverrides)
+ .build();
+ VespaModelCreatorWithMockPkg newModelCreator = new VespaModelCreatorWithMockPkg(newApp);
+ DeployState.Builder deployStateBuilder = new DeployState.Builder()
+ .applicationPackage(newApp)
+ .properties(new DeployProperties.Builder().hostedVespa(true).build())
+ .modelHostProvisioner(new InMemoryProvisioner(nodeCount))
+ .now(now);
+ if (previousModel != null)
+ deployStateBuilder.previousModel(previousModel);
+ VespaModel newModel = newModelCreator.create(deployStateBuilder);
+ return new Pair<>(newModel, newModelCreator.configChangeActions);
+ }
+
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java
new file mode 100644
index 00000000000..5126ada7818
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ClusterSizeReductionValidatorTest.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ConfigChangeRefeedAction;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationTester;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class ClusterSizeReductionValidatorTest {
+
+ @Test
+ public void testSizeReductionValidation() throws IOException, SAXException {
+ ValidationTester tester = new ValidationTester(30);
+
+ VespaModel previous = tester.deploy(null, getServices(30), null).getFirst();
+ try {
+ tester.deploy(previous, getServices(14), null);
+ fail("Expected exception due to cluster size reduction");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("cluster-size-reduction: Size reduction in 'default' is too large. Current size: 30, new size: 14. New size must be at least 50% of the current size",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void testSizeReductionValidationMinimalDecreaseIsAllowed() throws IOException, SAXException {
+ ValidationTester tester = new ValidationTester(30);
+
+ VespaModel previous = tester.deploy(null, getServices(3), null).getFirst();
+ tester.deploy(previous, getServices(2), null);
+ }
+
+ /*
+ @Test
+ public void testSizeReductionTo50PercentIsAllowed() throws IOException, SAXException {
+ ValidationTester tester = new ValidationTester(30);
+
+ VespaModel previous = tester.deploy(null, getServices(30), null).getFirst();
+ tester.deploy(previous, getServices(15), null);
+ }
+ */
+
+ @Test
+ public void testOverridingSizereductionValidation() throws IOException, SAXException {
+ ValidationTester tester = new ValidationTester(30);
+
+ VespaModel previous = tester.deploy(null, getServices(30), null).getFirst();
+ tester.deploy(previous, getServices(14), sizeReductionOverride); // Allowed due to override
+ }
+
+ private static String getServices(int size) {
+ return "<services version='1.0'>" +
+ " <content id='default' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='" + size + "'/>" +
+ " </content>" +
+ "</services>";
+ }
+
+ private static final String sizeReductionOverride =
+ "<validation-overrides>\n" +
+ " <allow until='2000-01-03'>cluster-size-reduction</allow>\n" +
+ "</validation-overrides>\n";
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigChangeTestUtils.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigChangeTestUtils.java
new file mode 100644
index 00000000000..8f1b0ca1cc1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigChangeTestUtils.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.application.validation.change.VespaRefeedAction;
+import com.yahoo.vespa.model.application.validation.change.VespaRestartAction;
+
+import java.util.List;
+
+public class ConfigChangeTestUtils {
+
+ public static VespaConfigChangeAction newRestartAction(String message) {
+ return new VespaRestartAction(message);
+ }
+
+ public static VespaConfigChangeAction newRestartAction(String message, List<ServiceInfo> services) {
+ return new VespaRestartAction(message, services);
+ }
+
+ public static VespaConfigChangeAction newRefeedAction(String name, ValidationOverrides overrides, String message) {
+ return VespaRefeedAction.of(name, overrides, message);
+ }
+
+ public static VespaConfigChangeAction newRefeedAction(String name, ValidationOverrides overrides, String message,
+ List<ServiceInfo> services, String documentType) {
+ return VespaRefeedAction.of(name, overrides, message, services, documentType);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidatorTest.java
new file mode 100644
index 00000000000..4375b5661b2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ConfigValueChangeValidatorTest.java
@@ -0,0 +1,297 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.test.AnotherrestartConfig;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.test.RestartConfig;
+import com.yahoo.test.SimpletypesConfig;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.RestartConfigs;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.test.utils.DeployLoggerStub;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Testing the validator on both a stub model and a real-life Vespa model.
+ *
+ * @author bjorncs
+ */
+public class ConfigValueChangeValidatorTest {
+
+ private DeployLoggerStub logger;
+
+ @Before
+ public void resetLogger() {
+ logger = new DeployLoggerStub();
+ }
+
+ /**
+ * NOTE: This test method has the following assumptions about the {@link com.yahoo.vespa.model.VespaModel}:
+ * 1) verbosegc and heapsize in qr-start.def is marked with restart.
+ * 2) {@link com.yahoo.vespa.model.container.Container} class has QrStartConfig listed in its
+ * {@link com.yahoo.vespa.model.application.validation.RestartConfigs} attribute.
+ * 3) That the config ids for the container services have a specific value.
+ *
+ * This test will to a certain degree ensure that the annotations in the VespaModel is correctly applied.
+ */
+ @Test
+ public void requireThatValidatorHandlesVespaModel() {
+ List<ConfigChangeAction> changes = getConfigChanges(
+ createVespaModel(createQrStartConfigSegment(true, 2096)),
+ createVespaModel(createQrStartConfigSegment(false, 2096))
+ );
+ assertEquals(3, changes.size());
+ assertComponentsEquals(changes, "default/container.0", 0);
+ assertComponentsEquals(changes, "admin/cluster-controllers/0", 1);
+ assertComponentsEquals(changes, "docproc/cluster.basicsearch.indexing/0", 2);
+ }
+
+ @Test
+ public void requireThatValidatorDetectsConfigChangeFromService() {
+ MockRoot oldRoot = createRootWithChildren(new SimpleConfigProducer("p", 0)
+ .withChildren(new ServiceWithAnnotation("s1", 1), new ServiceWithAnnotation("s2", 2)));
+ MockRoot newRoot = createRootWithChildren(new SimpleConfigProducer("p", 0)
+ .withChildren(new ServiceWithAnnotation("s1", 3), new ServiceWithAnnotation("s2", 4)));
+ List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
+ assertEquals(2, changes.size());
+ assertComponentsEquals(changes, "p/s1", 0);
+ assertComponentsEquals(changes, "p/s2", 1);
+ assertEquals("anotherrestart.anothervalue has changed from 1 to 3", changes.get(0).getMessage());
+ assertEquals("anotherrestart.anothervalue has changed from 2 to 4", changes.get(1).getMessage());
+ assertEmptyLog();
+ }
+
+ @Test
+ public void requireThatValidatorDetectsConfigChangeFromParentProducer() {
+ MockRoot oldRoot = createRootWithChildren(new SimpleConfigProducer("p", 1)
+ .withChildren(new ServiceWithAnnotation("s1", 0), new ServiceWithAnnotation("s2", 0)));
+ MockRoot newRoot = createRootWithChildren(new SimpleConfigProducer("p", 2)
+ .withChildren(new ServiceWithAnnotation("s1", 0), new ServiceWithAnnotation("s2", 0)));
+ List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
+ assertEquals(2, changes.size());
+ assertComponentsEquals(changes, "p/s1", 0);
+ assertComponentsEquals(changes, "p/s2", 1);
+ assertEmptyLog();
+ }
+
+ @Test
+ public void requireThatValidatorHandlesModelsWithDifferentTopology() {
+ MockRoot oldRoot = createRootWithChildren(
+ new SimpleConfigProducer("p1", 0).withChildren(new ServiceWithAnnotation("s1", 1)),
+ new SimpleConfigProducer("p2", 0).withChildren(new ServiceWithAnnotation("s2", 1)));
+ MockRoot newRoot = createRootWithChildren(
+ new ServiceWithAnnotation("s1", 2),
+ new ServiceWithAnnotation("s2", 2),
+ new ServiceWithAnnotation("s3", 2)
+ );
+
+ List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
+ assertTrue(changes.isEmpty());
+ assertEmptyLog();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void requireThatAnnotationDoesNotHaveEmtpyConfigList() {
+ MockRoot root = createRootWithChildren(new EmptyConfigListAnnotationService(""));
+ getConfigChanges(root, root);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void requireThatConfigHasRestartMethods() {
+ MockRoot root = createRootWithChildren(new ConfigWithMissingMethodsAnnotatedService(""));
+ getConfigChanges(root, root);
+ }
+
+ @Test
+ public void requireThatServicesAnnotatedWithNonRestartConfigProduceWarningInLog() {
+ MockRoot root = createRootWithChildren(new NonRestartConfigAnnotatedService(""));
+ getConfigChanges(root, root);
+ assertEquals(1, logger.entries.size());
+ }
+
+ @Test
+ public void requireThatConfigsFromAnnotatedSuperClassesAreDetected() {
+ MockRoot oldRoot = createRootWithChildren(new SimpleConfigProducer("p", 1).withChildren(
+ new ChildServiceWithAnnotation("child1", 0),
+ new ChildServiceWithoutAnnotation("child2", 0)));
+ MockRoot newRoot = createRootWithChildren(new SimpleConfigProducer("p", 2).withChildren(
+ new ChildServiceWithAnnotation("child1", 0),
+ new ChildServiceWithoutAnnotation("child2", 0)));
+ List<ConfigChangeAction> changes = getConfigChanges(oldRoot, newRoot);
+ assertEquals(2, changes.size());
+ assertComponentsEquals(changes, "p/child1", 0);
+ assertComponentsEquals(changes, "p/child2", 1);
+ assertEmptyLog();
+ }
+
+ private List<ConfigChangeAction> getConfigChanges(VespaModel currentModel, VespaModel nextModel) {
+ ConfigValueChangeValidator validator = new ConfigValueChangeValidator(logger);
+ return validator.validate(currentModel, nextModel, ValidationOverrides.empty());
+ }
+
+ private List<ConfigChangeAction> getConfigChanges(AbstractConfigProducerRoot currentModel,
+ AbstractConfigProducerRoot nextModel) {
+ ConfigValueChangeValidator validator = new ConfigValueChangeValidator(logger);
+ return validator.findConfigChangesFromModels(currentModel, nextModel).collect(Collectors.toList());
+ }
+
+ private static void assertComponentsEquals(List<ConfigChangeAction> changes, String name, int index) {
+ assertEquals(name, changes.get(index).getServices().get(0).getConfigId());
+ }
+
+ private void assertEmptyLog() {
+ assertTrue(logger.entries.isEmpty());
+ }
+
+ private static VespaModel createVespaModel(String configSegment) {
+ // Note that the configSegment is here located on root.
+ return new VespaModelCreatorWithMockPkg(
+ null,
+ "<services version='1.0'>\n" +
+ configSegment +
+ " <admin version='2.0'>\n" +
+ " <adminserver hostalias='node1'/>\n" +
+ " </admin>\n" +
+ " <jdisc id='default' version='1.0'>\n" +
+ " <search/>\n" +
+ " <nodes>\n" +
+ " <node hostalias='node1'/>\n" +
+ " </nodes>\n" +
+ " </jdisc>\n" +
+ " <content id='basicsearch' version='1.0'>\n" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>\n" +
+ " <document type='music' mode='index'/>\n" +
+ " </documents>\n" +
+ " <group>\n" +
+ " <node hostalias='node1' distribution-key='0'/>\n" +
+ " </group>\n" +
+ " <engine>\n" +
+ " <proton>\n" +
+ " <searchable-copies>1</searchable-copies>\n" +
+ " </proton>\n" +
+ " </engine>\n" +
+ " </content>\n" +
+ "</services>",
+ Collections.singletonList("search music { document music { } }")
+ ).create();
+ }
+
+ private static String createQrStartConfigSegment(boolean verboseGc, int heapsize) {
+ return "<config name='search.config.qr-start'>\n" +
+ " <jvm>\n" +
+ " <verbosegc>" + verboseGc + "</verbosegc>\n" +
+ " <heapsize>" + heapsize + "</heapsize>\n" +
+ " </jvm>" +
+ "</config>\n";
+ }
+
+ private static MockRoot createRootWithChildren(AbstractConfigProducer<?>... children) {
+ MockRoot root = new MockRoot();
+ Arrays.asList(children).forEach(root::addChild);
+ root.freezeModelTopology();
+ return root;
+ }
+
+ private static class NonRestartConfig extends ConfigInstance {}
+
+ private static abstract class ServiceStub extends AbstractService {
+ public ServiceStub(String name) {
+ super(name);
+ setHostResource(new HostResource(new Host(null, "localhost")));
+ }
+
+ @Override
+ public int getPortCount() {
+ return 0;
+ }
+ }
+
+ private static class SimpleConfigProducer extends AbstractConfigProducer<AbstractConfigProducer<?>>
+ implements RestartConfig.Producer {
+ public final int value;
+
+ public SimpleConfigProducer(String name, int value) {
+ super(name);
+ this.value = value;
+ }
+
+ @Override
+ public void getConfig(RestartConfig.Builder builder) {
+ builder.value(value);
+ }
+
+ public SimpleConfigProducer withChildren(AbstractConfigProducer<?>... producer) {
+ Arrays.asList(producer).forEach(this::addChild);
+ return this;
+ }
+ }
+
+
+ @RestartConfigs({RestartConfig.class, AnotherrestartConfig.class})
+ private static class ServiceWithAnnotation extends ServiceStub implements AnotherrestartConfig.Producer {
+ public final int anotherValue;
+
+ public ServiceWithAnnotation(String name, int anotherValue) {
+ super(name);
+ this.anotherValue = anotherValue;
+ }
+
+ @Override
+ public void getConfig(AnotherrestartConfig.Builder builder) {
+ builder.anothervalue(anotherValue);
+ }
+ }
+
+ @RestartConfigs(AnotherrestartConfig.class)
+ private static class ChildServiceWithAnnotation extends ServiceWithAnnotation {
+ public ChildServiceWithAnnotation(String name, int anotherValue) {
+ super(name, anotherValue);
+ }
+ }
+
+ private static class ChildServiceWithoutAnnotation extends ServiceWithAnnotation {
+ public ChildServiceWithoutAnnotation(String name, int anotherValue) {
+ super(name, anotherValue);
+ }
+ }
+
+ @RestartConfigs(SimpletypesConfig.class)
+ private static class NonRestartConfigAnnotatedService extends ServiceStub {
+ public NonRestartConfigAnnotatedService(String name) {
+ super(name);
+ }
+ }
+
+ @RestartConfigs(NonRestartConfig.class)
+ private static class ConfigWithMissingMethodsAnnotatedService extends ServiceStub {
+ public ConfigWithMissingMethodsAnnotatedService(String name) {
+ super(name);
+ }
+ }
+
+ @RestartConfigs
+ private static class EmptyConfigListAnnotationService extends ServiceStub {
+ public EmptyConfigListAnnotationService(String name) {
+ super(name);
+ }
+ }
+}
+
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidatorTest.java
new file mode 100644
index 00000000000..88ba6d885b8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContainerRestartValidatorTest.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bjorncs
+ */
+public class ContainerRestartValidatorTest {
+
+ @Test
+ public void validator_returns_action_for_containers_with_restart_on_deploy_enabled() {
+ VespaModel current = createModel(true);
+ VespaModel next = createModel(true);
+ List<ConfigChangeAction> result = validateModel(current, next);
+ assertEquals(2, result.size());
+ }
+
+ @Test
+ public void validator_returns_empty_list_for_containers_with_restart_on_deploy_disabled() {
+ VespaModel current = createModel(false);
+ VespaModel next = createModel(false);
+ List<ConfigChangeAction> result = validateModel(current, next);
+ assertTrue(result.isEmpty());
+ }
+
+ private static List<ConfigChangeAction> validateModel(VespaModel current, VespaModel next) {
+ return new ContainerRestartValidator()
+ .validate(current, next, new ValidationOverrides(Collections.emptyList()));
+ }
+
+ private static VespaModel createModel(boolean restartOnDeploy) {
+ return new VespaModelCreatorWithMockPkg(
+ null,
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services version='1.0'>\n" +
+ " <jdisc id='cluster1' version='1.0'>\n" +
+ " <http>\n" +
+ " <server id='server1' port='" + Defaults.getDefaults().vespaWebServicePort() + "'/>\n" +
+ " </http>\n" +
+ " <config name='container.qr'>\n" +
+ " <restartOnDeploy>" + restartOnDeploy + "</restartOnDeploy>\n" +
+ " </config>\n" +
+ " </jdisc>\n" +
+ " <jdisc id='cluster2' version='1.0'>\n" +
+ " <http>\n" +
+ " <server id='server2' port='4090'/>\n" +
+ " </http>\n" +
+ " <config name='container.qr'>\n" +
+ " <restartOnDeploy>" + restartOnDeploy + "</restartOnDeploy>\n" +
+ " </config>\n" +
+ " </jdisc>\n" +
+ " <jdisc id='cluster3' version='1.0'>\n" +
+ " <http>\n" +
+ " <server id='server3' port='4100'/>\n" +
+ " </http>\n" +
+ " </jdisc>\n" +
+ "</services>"
+ ).create();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java
new file mode 100644
index 00000000000..1b0fc00cbbc
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/ContentClusterRemovalValidatorTest.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationTester;
+import com.yahoo.yolean.Exceptions;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * @author bratseth
+ */
+public class ContentClusterRemovalValidatorTest {
+
+ @Test
+ public void testContentRemovalValidation() throws IOException, SAXException {
+ ValidationTester tester = new ValidationTester();
+
+ VespaModel previous = tester.deploy(null, getServices("contentClusterId"), null).getFirst();
+ try {
+ tester.deploy(previous, getServices("newContentClusterId"), null);
+ fail("Expected exception due to content cluster id change");
+ }
+ catch (IllegalArgumentException expected) {
+ assertEquals("content-cluster-removal: Content cluster 'contentClusterId' is removed. This will cause loss of all data in this cluster",
+ Exceptions.toMessageString(expected));
+ }
+ }
+
+ @Test
+ public void testOverridingContentRemovalValidation() throws IOException, SAXException {
+ ValidationTester tester = new ValidationTester();
+
+ VespaModel previous = tester.deploy(null, getServices("contentClusterId"), null).getFirst();
+ tester.deploy(previous, getServices("newContentClusterId"), removalOverride); // Allowed due to override
+ }
+
+ private static String getServices(String contentClusterId) {
+ return "<services version='1.0'>" +
+ " <content id='" + contentClusterId + "' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='1'/>" +
+ " </content>" +
+ "</services>";
+ }
+
+ private static final String removalOverride =
+ "<validation-overrides>\n" +
+ " <allow until='2000-01-03'>content-cluster-removal</allow>\n" +
+ "</validation-overrides>\n";
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidatorTest.java
new file mode 100644
index 00000000000..8e15ca27a61
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexedSearchClusterChangeValidatorTest.java
@@ -0,0 +1,175 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.IndexedSearchClusterChangeValidator;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.application.validation.change.VespaRefeedAction;
+import com.yahoo.vespa.model.content.utils.ApplicationPackageBuilder;
+import com.yahoo.vespa.model.content.utils.ContentClusterBuilder;
+import com.yahoo.vespa.model.content.utils.SearchDefinitionBuilder;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.newRestartAction;
+import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.newRefeedAction;
+
+public class IndexedSearchClusterChangeValidatorTest {
+
+ static class Fixture {
+ VespaModel currentModel;
+ VespaModel nextModel;
+ IndexedSearchClusterChangeValidator validator;
+
+ public Fixture(VespaModel currentModel, VespaModel nextModel) {
+ this.currentModel = currentModel;
+ this.nextModel = nextModel;
+ validator = new IndexedSearchClusterChangeValidator();
+ }
+
+ public static Fixture newOneDocFixture(String currentSd, String nextSd) {
+ return new Fixture(newOneDocModel(currentSd), newOneDocModel(nextSd));
+ }
+
+ public static VespaModel newOneDocModel(String sdContent) {
+ return new ApplicationPackageBuilder().
+ addCluster(new ContentClusterBuilder().name("foo").docTypes(Arrays.asList("d1"))).
+ addSearchDefinition(new SearchDefinitionBuilder().
+ name("d1").content(sdContent).build()).buildCreator().create();
+ }
+
+ public static Fixture newTwoDocFixture(String currentSd, String nextSd) {
+ return new Fixture(newTwoDocModel(currentSd, currentSd), newTwoDocModel(nextSd, nextSd));
+ }
+
+ public static VespaModel newTwoDocModel(String d1Content, String d2Content) {
+ return new ApplicationPackageBuilder().
+ addCluster(new ContentClusterBuilder().name("foo").docTypes(Arrays.asList("d1", "d2"))).
+ addSearchDefinition(new SearchDefinitionBuilder().
+ name("d1").content(d1Content).build()).
+ addSearchDefinition(new SearchDefinitionBuilder().
+ name("d2").content(d2Content).build()).
+ buildCreator().create();
+ }
+
+ public static Fixture newTwoClusterFixture(String currentSd, String nextSd) {
+ return new Fixture(newTwoClusterModel(currentSd, currentSd), newTwoClusterModel(nextSd, nextSd));
+ }
+
+ public static VespaModel newTwoClusterModel(String d1Content, String d2Content) {
+ return new ApplicationPackageBuilder().
+ addCluster(new ContentClusterBuilder().name("foo").docTypes(Arrays.asList("d1"))).
+ addCluster(new ContentClusterBuilder().name("bar").docTypes(Arrays.asList("d2"))).
+ addSearchDefinition(new SearchDefinitionBuilder().
+ name("d1").content(d1Content).build()).
+ addSearchDefinition(new SearchDefinitionBuilder().
+ name("d2").content(d2Content).build()).
+ buildCreator().create();
+ }
+
+ public void assertValidation() {
+ List<ConfigChangeAction> act = normalizeServicesInActions(validator.validate(currentModel, nextModel,
+ ValidationOverrides.empty()));
+ assertThat(act.size(), is(0));
+ }
+
+ private static List<ConfigChangeAction> normalizeServicesInActions(List<ConfigChangeAction> result) {
+ return result.stream().
+ map(action -> ((VespaConfigChangeAction) action).modifyAction(
+ action.getMessage(),
+ normalizeServices(action.getServices()),
+ action.getType().equals(ConfigChangeAction.Type.REFEED) ?
+ ((VespaRefeedAction)action).getDocumentType() : "")).
+ collect(Collectors.toList());
+ }
+
+ private static List<ServiceInfo> normalizeServices(List<ServiceInfo> services) {
+ return services.stream().
+ map(service -> new ServiceInfo(service.getServiceName(), "null", null, null,
+ service.getConfigId(), "null")).
+ collect(Collectors.toList());
+ }
+
+ public void assertValidation(ConfigChangeAction exp) {
+ assertValidation(Arrays.asList(exp));
+ }
+
+ public void assertValidation(List<ConfigChangeAction> exp) {
+ List<ConfigChangeAction> act = normalizeServicesInActions(validator.validate(currentModel, nextModel,
+ ValidationOverrides.empty()));
+ exp.sort((lhs, rhs) -> lhs.getMessage().compareTo(rhs.getMessage()));
+ act.sort((lhs, rhs) -> lhs.getMessage().compareTo(rhs.getMessage()));
+ assertThat(act, equalTo(exp));
+ }
+ }
+
+ static String STRING_FIELD = "field f1 type string { indexing: summary }";
+ static String ATTRIBUTE_FIELD = "field f1 type string { indexing: attribute | summary }";
+ static String ATTRIBUTE_CHANGE_MSG = "Field 'f1' changed: add attribute aspect";
+ static String INT_FIELD = "field f1 type int { indexing: summary }";
+ static String FIELD_TYPE_CHANGE_MSG = "Field 'f1' changed: data type: 'string' -> 'int'";
+ private static List<ServiceInfo> FOO_SERVICE = Arrays.asList(
+ new ServiceInfo("searchnode", "null", null, null, "foo/search/cluster.foo/0", "null"));
+ private static List<ServiceInfo> BAR_SERVICE = Arrays.asList(
+ new ServiceInfo("searchnode2", "null", null, null, "bar/search/cluster.bar/0", "null"));
+
+ @Test
+ public void requireThatDocumentDatabaseChangeIsDiscovered() {
+ Fixture.newOneDocFixture(STRING_FIELD, ATTRIBUTE_FIELD).
+ assertValidation(newRestartAction("Document type 'd1': " + ATTRIBUTE_CHANGE_MSG, FOO_SERVICE));
+ }
+
+ @Test
+ public void requireThatChangeInSeveralDocumentDatabasesAreDiscovered() {
+ Fixture.newTwoDocFixture(STRING_FIELD, ATTRIBUTE_FIELD).
+ assertValidation(Arrays.asList(newRestartAction("Document type 'd1': "
+ + ATTRIBUTE_CHANGE_MSG, FOO_SERVICE),
+ newRestartAction("Document type 'd2': " + ATTRIBUTE_CHANGE_MSG, FOO_SERVICE)));
+ }
+
+ @Test
+ public void requireThatChangeInSeveralContentClustersAreDiscovered() {
+ Fixture.newTwoClusterFixture(STRING_FIELD, ATTRIBUTE_FIELD).
+ assertValidation(Arrays.asList(newRestartAction("Document type 'd1': "
+ + ATTRIBUTE_CHANGE_MSG, FOO_SERVICE),
+ newRestartAction("Document type 'd2': " + ATTRIBUTE_CHANGE_MSG, BAR_SERVICE)));
+ }
+
+ @Test
+ public void requireThatAddingDocumentDatabaseIsOk() {
+ new Fixture(Fixture.newOneDocModel(STRING_FIELD), Fixture.newTwoDocModel(STRING_FIELD, STRING_FIELD)).assertValidation();
+ }
+
+ @Test
+ public void requireThatRemovingDocumentDatabaseIsOk() {
+ new Fixture(Fixture.newTwoDocModel(STRING_FIELD, STRING_FIELD), Fixture.newOneDocModel(STRING_FIELD)).assertValidation();
+ }
+
+ @Test
+ public void requireThatAddingContentClusterIsOk() {
+ new Fixture(Fixture.newOneDocModel(STRING_FIELD), Fixture.newTwoClusterModel(STRING_FIELD, STRING_FIELD)).assertValidation();
+ }
+
+ @Test
+ public void requireThatRemovingContentClusterIsOk() {
+ new Fixture(Fixture.newTwoClusterModel(STRING_FIELD, STRING_FIELD), Fixture.newOneDocModel(STRING_FIELD)).assertValidation();
+ }
+
+ @Test
+ public void requireThatChangingFieldTypeIsDiscovered() {
+ Fixture f = Fixture.newOneDocFixture(STRING_FIELD, INT_FIELD);
+ f.assertValidation(Arrays.asList(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Document type 'd1': " + FIELD_TYPE_CHANGE_MSG, FOO_SERVICE, "d1")));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java
new file mode 100644
index 00000000000..5a358639af3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/IndexingModeChangeValidatorTest.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.api.ConfigChangeRefeedAction;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.ValidationTester;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bratseth
+ */
+public class IndexingModeChangeValidatorTest {
+
+ @Test
+ public void testChangingIndexMode() throws IOException, SAXException {
+ ValidationTester tester = new ValidationTester();
+
+ VespaModel oldModel =
+ tester.deploy(null, getServices(AbstractSearchCluster.IndexingMode.REALTIME), validationOverrides).getFirst();
+ List<ConfigChangeAction> changeActions =
+ tester.deploy(oldModel, getServices(AbstractSearchCluster.IndexingMode.STREAMING), validationOverrides).getSecond();
+
+ assertRefeedChange(true, // allowed=true due to validation override
+ "Cluster 'default' changed indexing mode from 'indexed' to 'streaming'",
+ changeActions);
+ }
+
+ private void assertRefeedChange(boolean allowed, String message, List<ConfigChangeAction> changeActions) {
+ List<ConfigChangeAction> refeedActions = changeActions.stream()
+ .filter(a -> a instanceof ConfigChangeRefeedAction)
+ .collect(Collectors.toList());
+ assertEquals(1, refeedActions.size());
+ assertEquals(allowed, refeedActions.get(0).allowed());
+ assertTrue(refeedActions.get(0) instanceof ConfigChangeRefeedAction);
+ assertEquals("indexing-mode-change", ((ConfigChangeRefeedAction)refeedActions.get(0)).name());
+ assertEquals(message, refeedActions.get(0).getMessage());
+ }
+
+ private static final String getServices(AbstractSearchCluster.IndexingMode indexingMode) {
+ return "<services version='1.0'>" +
+ " <content id='default' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <engine>" +
+ (indexingMode.equals(AbstractSearchCluster.IndexingMode.REALTIME) ? " <proton/>" : " <vds/>") +
+ " </engine>" +
+ " <documents>" +
+ " <document type='music' mode='" +
+ (indexingMode.equals(AbstractSearchCluster.IndexingMode.REALTIME) ? "index" : "streaming") + "'/>" +
+ " </documents>" +
+ " <nodes count='1'/>" +
+ " </content>" +
+ "</services>";
+ }
+
+ private static final String validationOverrides =
+ "<validation-overrides>\n" +
+ " <allow until='2000-01-14' comment='test override'>indexing-mode-change</allow>\n" +
+ "</validation-overrides>\n";
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidatorTest.java
new file mode 100644
index 00000000000..764a5e57783
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/StartupCommandChangeValidatorTest.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change;
+
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.application.validation.change.StartupCommandChangeValidator;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.*;
+
+public class StartupCommandChangeValidatorTest {
+
+ @Test
+ public void requireThatDifferentStartupCommandIsDetected() {
+ MockRoot oldRoot = createRootWithChildren(new ServiceStub("evilservice", "rm -rf /"));
+ MockRoot newRoot = createRootWithChildren(new ServiceStub("evilservice", "rm -rf *"));
+ List<ConfigChangeAction> changes = getStartupCommandChanges(oldRoot, newRoot);
+ assertEquals(1, changes.size());
+ assertEquals("evilservice", changes.get(0).getServices().get(0).getConfigId());
+ }
+
+ @Test
+ public void requireEmptyResultForEqualStartupCommand() {
+ MockRoot oldRoot = createRootWithChildren(new ServiceStub("evilservice", "./hax.sh"));
+ MockRoot newRoot = createRootWithChildren(new ServiceStub("evilservice", "./hax.sh"));
+ List<ConfigChangeAction> changes = getStartupCommandChanges(oldRoot, newRoot);
+ assertTrue(changes.isEmpty());
+ }
+
+ @Test
+ public void requireEmptyResultForDifferentServices() {
+ MockRoot oldRoot = createRootWithChildren(new ServiceStub("evilservice", "./hax.sh"));
+ MockRoot newRoot = createRootWithChildren(new ServiceStub("goodservice", "./hax.sh"));
+ List<ConfigChangeAction> changes = getStartupCommandChanges(oldRoot, newRoot);
+ assertTrue(changes.isEmpty());
+ }
+
+ private static List<ConfigChangeAction> getStartupCommandChanges(
+ AbstractConfigProducerRoot currentModel, AbstractConfigProducerRoot nextModel) {
+ StartupCommandChangeValidator validator = new StartupCommandChangeValidator();
+ return validator.findServicesWithChangedStartupCommmand(currentModel, nextModel).collect(Collectors.toList());
+ }
+
+ private static MockRoot createRootWithChildren(AbstractConfigProducer<?>... children) {
+ MockRoot root = new MockRoot();
+ Arrays.asList(children).forEach(root::addChild);
+ root.freezeModelTopology();
+ return root;
+ }
+
+ private static class ServiceStub extends AbstractService {
+ private final String startupCommand;
+
+ public ServiceStub(String name, String startupCommand) {
+ super(name);
+ setHostResource(new HostResource(new Host(null, "localhost")));
+ this.startupCommand = startupCommand;
+ }
+
+ @Override
+ public String getStartupCommand() {
+ return startupCommand;
+ }
+
+ @Override
+ public int getPortCount() {
+ return 0;
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java
new file mode 100644
index 00000000000..8271e0409d7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/AttributeChangeValidatorTest.java
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import org.junit.Test;
+
+import java.util.List;
+
+import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.newRestartAction;
+
+public class AttributeChangeValidatorTest {
+
+ private static class Fixture extends ContentClusterFixture {
+ AttributeChangeValidator validator;
+
+ public Fixture(String currentSd, String nextSd) throws Exception {
+ super(currentSd, nextSd);
+ validator = new AttributeChangeValidator(currentDb().getDerivedConfiguration().getAttributeFields(),
+ currentDb().getDerivedConfiguration().getIndexSchema(),
+ currentDocType(),
+ nextDb().getDerivedConfiguration().getAttributeFields(),
+ nextDb().getDerivedConfiguration().getIndexSchema(),
+ nextDocType());
+ }
+
+ @Override
+ public List<VespaConfigChangeAction> validate() {
+ return validator.validate();
+ }
+
+ }
+
+ @Test
+ public void requireThatAddingAttributeAspectRequireRestart() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: summary }",
+ "field f1 type string { indexing: attribute | summary }");
+ f.assertValidation(newRestartAction(
+ "Field 'f1' changed: add attribute aspect"));
+ }
+
+ @Test
+ public void requireThatRemovingAttributeAspectRequireRestart() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: attribute | summary }",
+ "field f1 type string { indexing: summary }");
+ f.assertValidation(newRestartAction(
+ "Field 'f1' changed: remove attribute aspect"));
+ }
+
+ @Test
+ public void requireThatAddingAttributeFieldIsOk() throws Exception {
+ Fixture f = new Fixture("", "field f1 type string { indexing: attribute | summary \n attribute: fast-search }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatRemovingAttributeFieldIsOk() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: attribute | summary }", "");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatChangingFastSearchRequireRestart() throws Exception {
+ new Fixture("field f1 type string { indexing: attribute }",
+ "field f1 type string { indexing: attribute \n attribute: fast-search }").
+ assertValidation(newRestartAction(
+ "Field 'f1' changed: add attribute 'fast-search'"));
+ }
+
+ @Test
+ public void requireThatChangingFastAccessRequireRestart() throws Exception {
+ new Fixture("field f1 type string { indexing: attribute \n attribute: fast-access }",
+ "field f1 type string { indexing: attribute }").
+ assertValidation(newRestartAction(
+ "Field 'f1' changed: remove attribute 'fast-access'"));
+ }
+
+ @Test
+ public void requireThatChangingHugeRequireRestart() throws Exception {
+ new Fixture("field f1 type string { indexing: attribute }",
+ "field f1 type string { indexing: attribute \n attribute: huge }").
+ assertValidation(newRestartAction(
+ "Field 'f1' changed: add attribute 'huge'"));
+ }
+
+ @Test
+ public void requireThatChangingDensePostingListThresholdRequireRestart() throws Exception {
+ new Fixture(
+ "field f1 type predicate { indexing: attribute \n index { arity: 8 \n dense-posting-list-threshold: 0.2 } }",
+ "field f1 type predicate { indexing: attribute \n index { arity: 8 \n dense-posting-list-threshold: 0.4 } }").
+ assertValidation(newRestartAction(
+ "Field 'f1' changed: change property 'dense-posting-list-threshold' from '0.2' to '0.4'"));
+ }
+
+ @Test
+ public void requireThatRemovingAttributeAspectFromIndexFieldIsOk() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: index | attribute }",
+ "field f1 type string { indexing: index }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatRemovingAttributeAspectFromIndexAndSummaryFieldIsOk() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: index | attribute | summary }",
+ "field f1 type string { indexing: index | summary }");
+ f.assertValidation();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/ContentClusterFixture.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/ContentClusterFixture.java
new file mode 100644
index 00000000000..086544ba6ef
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/ContentClusterFixture.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.utils.ContentClusterBuilder;
+import com.yahoo.vespa.model.content.utils.ContentClusterUtils;
+import com.yahoo.vespa.model.content.utils.SearchDefinitionBuilder;
+import com.yahoo.vespa.model.search.DocumentDatabase;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Test fixture to setup current and next content clusters used for change validation.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public abstract class ContentClusterFixture {
+ protected ContentCluster currentCluster;
+ protected ContentCluster nextCluster;
+
+ public ContentClusterFixture(String currentSd, String nextSd) throws Exception {
+ currentCluster = createCluster(currentSd);
+ nextCluster = createCluster(nextSd);
+ }
+
+ private static ContentCluster createCluster(String sdContent) throws Exception {
+ return new ContentClusterBuilder().build(
+ ContentClusterUtils.createMockRoot(
+ Arrays.asList(new SearchDefinitionBuilder().content(sdContent).build())));
+ }
+
+ protected DocumentDatabase currentDb() {
+ return currentCluster.getSearch().getIndexed().getDocumentDbs().get(0);
+ }
+
+ protected NewDocumentType currentDocType() {
+ return currentCluster.getDocumentDefinitions().get("test");
+ }
+
+ protected DocumentDatabase nextDb() {
+ return nextCluster.getSearch().getIndexed().getDocumentDbs().get(0);
+ }
+
+ protected NewDocumentType nextDocType() {
+ return nextCluster.getDocumentDefinitions().get("test");
+ }
+
+ public void assertValidation() {
+ List<VespaConfigChangeAction> act = validate();
+ assertThat(act.size(), is(0));
+ }
+
+ public void assertValidation(VespaConfigChangeAction exp) {
+ assertValidation(Arrays.asList(exp));
+ }
+
+ public void assertValidation(List<VespaConfigChangeAction> exp) {
+ List<VespaConfigChangeAction> act = validate();
+ assertThat(act, equalTo(exp));
+ }
+
+ public abstract List<VespaConfigChangeAction> validate();
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidatorTest.java
new file mode 100644
index 00000000000..4391878c5be
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentDatabaseChangeValidatorTest.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.newRestartAction;
+import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.newRefeedAction;
+
+public class DocumentDatabaseChangeValidatorTest {
+
+ private static class Fixture extends ContentClusterFixture {
+ DocumentDatabaseChangeValidator validator;
+
+ public Fixture(String currentSd, String nextSd) throws Exception {
+ super(currentSd, nextSd);
+ validator = new DocumentDatabaseChangeValidator(currentDb(), currentDocType(), nextDb(), nextDocType());
+ }
+
+ @Override
+ public List<VespaConfigChangeAction> validate() {
+ return validator.validate(ValidationOverrides.empty());
+ }
+
+ }
+
+ @Test
+ public void requireThatAttributeIndexAndDocumentTypeChangesAreDiscovered() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: summary } " +
+ "field f2 type string { indexing: summary } " +
+ "field f3 type int { indexing: summary }",
+ "field f1 type string { indexing: attribute | summary } " +
+ "field f2 type string { indexing: index | summary } " +
+ "field f3 type string { indexing: summary }");
+ f.assertValidation(Arrays.asList(
+ newRestartAction("Field 'f1' changed: add attribute aspect"),
+ newRefeedAction("indexing-change",
+ ValidationOverrides.empty(),
+ "Field 'f2' changed: add index aspect, indexing script: '{ input f2 | summary f2; }' -> " +
+ "'{ input f2 | tokenize normalize stem:\"SHORTEST\" | index f2 | summary f2; }'"),
+ newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f3' changed: data type: 'int' -> 'string'")));
+ }
+
+ @Test
+ public void requireThatRemovingAttributeAspectFromIndexFieldIsOk() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: index | attribute }",
+ "field f1 type string { indexing: index }");
+ f.assertValidation();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidatorTest.java
new file mode 100644
index 00000000000..89f56470f1d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/DocumentTypeChangeValidatorTest.java
@@ -0,0 +1,170 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.yahoo.vespa.model.application.validation.change.ConfigChangeTestUtils.newRefeedAction;
+
+/**
+ * Test validation of changes between a current and next document type used in a document database.
+ *
+ * @author <a href="mailto:Tor.Egge@yahoo-inc.com">Tor Egge</a>
+ * @since 2014-11-25
+ */
+public class DocumentTypeChangeValidatorTest {
+
+ private static class Fixture extends ContentClusterFixture {
+ DocumentTypeChangeValidator validator;
+
+ public Fixture(String currentSd, String nextSd) throws Exception {
+ super(currentSd, nextSd);
+ validator = new DocumentTypeChangeValidator(currentDocType(), nextDocType());
+ }
+
+ @Override
+ public List<VespaConfigChangeAction> validate() {
+ return validator.validate(ValidationOverrides.empty());
+ }
+
+ }
+
+ @Test
+ public void requireThatFieldRemovalIsOK() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: summary }",
+ "field f2 type string { indexing: summary }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatSameDataTypeIsOK() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: summary }",
+ "field f1 type string { indexing: summary }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatDataTypeChangeIsNotOK() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: summary }",
+ "field f1 type int { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f1' changed: data type: 'string' -> 'int'"));
+ }
+
+ @Test
+ public void requireThatAddingCollectionTypeIsNotOK() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: summary }",
+ "field f1 type array<string> { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f1' changed: data type: 'string' -> 'Array<string>'"));
+ }
+
+
+ @Test
+ public void requireThatSameNestedDataTypeIsOK() throws Exception {
+ Fixture f = new Fixture("field f1 type array<string> { indexing: summary }",
+ "field f1 type array<string> { indexing: summary }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatNestedDataTypeChangeIsNotOK() throws Exception {
+ Fixture f = new Fixture("field f1 type array<string> { indexing: summary }",
+ "field f1 type array<int> { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f1' changed: data type: 'Array<string>' -> 'Array<int>'"));
+ }
+
+ @Test
+ public void requireThatChangedCollectionTypeIsNotOK() throws Exception {
+ Fixture f = new Fixture("field f1 type array<string> { indexing: summary }",
+ "field f1 type weightedset<string> { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f1' changed: data type: 'Array<string>' -> 'WeightedSet<string>'"));
+ }
+
+ @Test
+ public void requireThatMultipleDataTypeChangesIsNotOK() throws Exception {
+ Fixture f = new Fixture("field f1 type string { indexing: summary } field f2 type int { indexing: summary }" ,
+ "field f2 type string { indexing: summary } field f1 type int { indexing: summary }");
+ f.assertValidation(Arrays.asList(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f1' changed: data type: 'string' -> 'int'"),
+ newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f2' changed: data type: 'int' -> 'string'")));
+ }
+
+ @Test
+ public void requireThatSameDataTypeInStructFieldIsOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type string {} } field f2 type s1 { indexing: summary }",
+ "struct s1 { field f1 type string {} } field f2 type s1 { indexing: summary }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatSameNestedDataTypeChangeInStructFieldIsOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type array<string> {} } field f2 type s1 { indexing: summary }",
+ "struct s1 { field f1 type array<string> {} } field f2 type s1 { indexing: summary }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatAddingFieldInStructFieldIsOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type string {} } field f3 type s1 { indexing: summary }",
+ "struct s1 { field f1 type string {} field f2 type int {} } field f3 type s1 { indexing: summary }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatRemovingFieldInStructFieldIsOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type string {} field f2 type int {} } field f3 type s1 { indexing: summary }",
+ "struct s1 { field f1 type string {} } field f3 type s1 { indexing: summary }");
+ f.assertValidation();
+ }
+
+ @Test
+ public void requireThatDataTypeChangeInStructFieldIsNotOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type string {} } field f2 type s1 { indexing: summary }",
+ "struct s1 { field f1 type int {} } field f2 type s1 { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f2' changed: data type: 's1:{f1:string}' -> 's1:{f1:int}'"));
+ }
+
+ @Test
+ public void requireThatNestedDataTypeChangeInStructFieldIsNotOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type array<string> {} } field f2 type s1 { indexing: summary }",
+ "struct s1 { field f1 type array<int> {} } field f2 type s1 { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f2' changed: data type: 's1:{f1:Array<string>}' -> 's1:{f1:Array<int>}'"));
+ }
+
+ @Test
+ public void requireThatDataTypeChangeInNestedStructFieldIsNotOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type string {} } struct s2 { field f2 type s1 {} } field f3 type s2 { indexing: summary }",
+ "struct s1 { field f1 type int {} } struct s2 { field f2 type s1 {} } field f3 type s2 { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f3' changed: data type: 's2:{s1:{f1:string}}' -> 's2:{s1:{f1:int}}'"));
+ }
+
+ @Test
+ public void requireThatMultipleDataTypeChangesInStructFieldIsNotOK() throws Exception {
+ Fixture f = new Fixture("struct s1 { field f1 type string {} field f2 type int {} } field f3 type s1 { indexing: summary }",
+ "struct s1 { field f1 type int {} field f2 type string {} } field f3 type s1 { indexing: summary }");
+ f.assertValidation(newRefeedAction("field-type-change",
+ ValidationOverrides.empty(),
+ "Field 'f3' changed: data type: 's1:{f1:string,f2:int}' -> 's1:{f1:int,f2:string}'"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidatorTest.java
new file mode 100644
index 00000000000..0dea99c7b01
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/application/validation/change/search/IndexingScriptChangeValidatorTest.java
@@ -0,0 +1,166 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.application.validation.change.search;
+
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.model.application.validation.ValidationOverrides;
+import com.yahoo.vespa.model.application.validation.change.VespaConfigChangeAction;
+import com.yahoo.vespa.model.application.validation.change.VespaRefeedAction;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+
+public class IndexingScriptChangeValidatorTest {
+
+ private static class Fixture extends ContentClusterFixture {
+ IndexingScriptChangeValidator validator;
+
+ public Fixture(String currentSd, String nextSd) throws Exception {
+ super(currentSd, nextSd);
+ validator = new IndexingScriptChangeValidator(currentDb().getDerivedConfiguration().getSearch(),
+ nextDb().getDerivedConfiguration().getSearch());
+ }
+
+ @Override
+ public List<VespaConfigChangeAction> validate() {
+ return validator.validate(ValidationOverrides.empty());
+ }
+ }
+
+ private static class ScriptFixture {
+ private final ScriptExpression currentScript;
+ private final ScriptExpression nextScript;
+
+ public ScriptFixture(String currentScript, String nextScript) throws Exception {
+ this.currentScript = ScriptExpression.fromString(currentScript);
+ this.nextScript = ScriptExpression.fromString(nextScript);
+ }
+
+ public boolean validate() {
+ return IndexingScriptChangeValidator.equalScripts(currentScript, nextScript);
+ }
+ }
+
+ private static String FIELD = "field f1 type string";
+ private static String FIELD_F2 = "field f2 type string";
+
+ private static VespaConfigChangeAction expectedAction(String changedMsg, String fromScript, String toScript) {
+ return expectedAction("f1", changedMsg, fromScript, toScript);
+ }
+
+ private static VespaConfigChangeAction expectedAction(String field, String changedMsg, String fromScript, String toScript) {
+ return VespaRefeedAction.of("indexing-change",
+ ValidationOverrides.empty(),
+ "Field '" + field + "' changed: " +
+ (changedMsg.isEmpty() ? "" : changedMsg + ", ") +
+ "indexing script: '" + fromScript + "' -> '" + toScript + "'");
+ }
+
+ @Test
+ public void requireThatAddingIndexAspectRequireRefeed() throws Exception {
+ new Fixture(FIELD + " { indexing: summary }",
+ FIELD + " { indexing: index | summary }").
+ assertValidation(expectedAction("add index aspect",
+ "{ input f1 | summary f1; }",
+ "{ input f1 | tokenize normalize stem:\"SHORTEST\" | index f1 | summary f1; }"));
+ }
+
+ @Test
+ public void requireThatRemovingIndexAspectRequireRefeed() throws Exception {
+ new Fixture(FIELD + " { indexing: index | summary }",
+ FIELD + " { indexing: summary }").
+ assertValidation(expectedAction("remove index aspect",
+ "{ input f1 | tokenize normalize stem:\"SHORTEST\" | index f1 | summary f1; }",
+ "{ input f1 | summary f1; }"));
+ }
+
+ @Test
+ public void requireThatChangingStemmingRequireRefeed() throws Exception {
+ new Fixture(FIELD + " { indexing: index }",
+ FIELD + " { indexing: index \n stemming: none }").
+ assertValidation(expectedAction("stemming: 'shortest' -> 'none'",
+ "{ input f1 | tokenize normalize stem:\"SHORTEST\" | index f1; }",
+ "{ input f1 | tokenize normalize | index f1; }"));
+ }
+
+ @Test
+ public void requireThatChangingNormalizingRequireRefeed() throws Exception {
+ new Fixture(FIELD + " { indexing: index }",
+ FIELD + " { indexing: index \n normalizing: none }").
+ assertValidation(expectedAction("normalizing: 'ACCENT' -> 'NONE'",
+ "{ input f1 | tokenize normalize stem:\"SHORTEST\" | index f1; }",
+ "{ input f1 | tokenize stem:\"SHORTEST\" | index f1; }"));
+ }
+
+ @Test
+ public void requireThatChangingMatchingRequireRefeed() throws Exception {
+ new Fixture(FIELD + " { indexing: index \n match: exact }",
+ FIELD + " { indexing: index \n match { gram \n gram-size: 3 } }").
+ assertValidation(expectedAction("matching: 'exact' -> 'gram (size 3)', normalizing: 'LOWERCASE' -> 'CODEPOINT'",
+ "{ input f1 | exact | index f1; }",
+ "{ input f1 | ngram 3 | index f1; }"));
+ }
+
+ @Test
+ public void requireThatSettingDynamicSummaryRequireRefeed() throws Exception {
+ new Fixture(FIELD + " { indexing: summary }",
+ FIELD + " { indexing: summary \n summary: dynamic }").
+ assertValidation(expectedAction("summary field 'f1' transform: 'none' -> 'dynamicteaser'",
+ "{ input f1 | summary f1; }",
+ "{ input f1 | tokenize normalize stem:\"SHORTEST\" | summary f1; }"));
+ }
+
+ @Test
+ public void requireThatMultipleChangesRequireRefeed() throws Exception {
+ new Fixture(FIELD + " { indexing: index } " + FIELD_F2 + " { indexing: index }",
+ FIELD + " { indexing: index \n stemming: none } " + FIELD_F2 + " { indexing: index \n normalizing: none }").
+ assertValidation(Arrays.asList(expectedAction("f1", "stemming: 'shortest' -> 'none'",
+ "{ input f1 | tokenize normalize stem:\"SHORTEST\" | index f1; }",
+ "{ input f1 | tokenize normalize | index f1; }"),
+ expectedAction("f2", "normalizing: 'ACCENT' -> 'NONE'",
+ "{ input f2 | tokenize normalize stem:\"SHORTEST\" | index f2; }",
+ "{ input f2 | tokenize stem:\"SHORTEST\" | index f2; }")));
+ }
+
+ @Test
+ public void requireThatAddingIndexFieldIsOk() throws Exception {
+ new Fixture("", "field f1 type string { indexing: index | summary }").
+ assertValidation();
+ }
+
+ @Test
+ public void requireThatRemovingIndexFieldIsOk() throws Exception {
+ new Fixture("field f1 type string { indexing: index | summary }", "").
+ assertValidation();
+ }
+
+ @Test
+ public void requireThatAddingFieldIsOk() throws Exception {
+ new Fixture("", FIELD + " { indexing: attribute | summary }").
+ assertValidation();
+ }
+
+ @Test
+ public void requireThatAddingSummaryAspectIsOk() throws Exception {
+ new Fixture(FIELD + " { indexing: attribute }",
+ FIELD + " { indexing: attribute | summary }").
+ assertValidation();
+ }
+
+ @Test
+ public void requireThatSettingDynamicSummaryOnIndexFieldIsOk() throws Exception {
+ new Fixture(FIELD + " { indexing: index | summary }",
+ FIELD + " { indexing: index | summary \n summary: dynamic }").
+ assertValidation();
+ }
+
+ @Test
+ public void requireThatOutputExpressionsAreIgnoredInAdvancedScript() throws Exception {
+ assertTrue(new ScriptFixture("{ input foo | switch { case \"audio\": input bar | index; case \"video\": input baz | index; default: 0 | index; }; }",
+ "{ input foo | switch { case \"audio\": input bar | attribute; case \"video\": input baz | attribute; default: 0 | attribute; }; }").
+ validate());
+ }
+
+} \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/UserConfigBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/UserConfigBuilderTest.java
new file mode 100644
index 00000000000..db35e2cac1e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/UserConfigBuilderTest.java
@@ -0,0 +1,128 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder;
+
+import com.yahoo.test.ArraytypesConfig;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.deploy.ConfigDefinitionStore;
+import com.yahoo.test.SimpletypesConfig;
+import com.yahoo.config.model.producer.UserConfigRepo;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.ConfigPayloadBuilder;
+import com.yahoo.vespa.configdefinition.SpecialtokensConfig;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class UserConfigBuilderTest {
+
+ private final ConfigDefinitionStore configDefinitionStore = new ConfigDefinitionStore() {
+ @Override
+ public ConfigDefinition getConfigDefinition(ConfigDefinitionKey defKey) { return null; }
+ };
+
+ @Test
+ public void require_that_simple_config_is_resolved() throws ParserConfigurationException, IOException, SAXException {
+ Element configRoot = getDocument("<config name=\"simpletypes\">" +
+ " <intval>13</intval>" +
+ "</config>" +
+ "<config name=\"simpletypes\" version=\"1\">" +
+ " <stringval>foolio</stringval>" +
+ "</config>");
+ UserConfigRepo map = UserConfigBuilder.build(configRoot, configDefinitionStore, new BaseDeployLogger());
+ assertFalse(map.isEmpty());
+ ConfigDefinitionKey key = new ConfigDefinitionKey("simpletypes", "config");
+ assertNotNull(map.get(key));
+ SimpletypesConfig config = createConfig(SimpletypesConfig.class, map.get(key));
+ assertThat(config.intval(), is(13));
+ assertThat(config.stringval(), is("foolio"));
+ }
+
+ public static <ConfigType extends ConfigInstance> ConfigType createConfig(Class<ConfigType> clazz, ConfigPayloadBuilder builder) {
+ return ConfigPayload.fromBuilder(builder).toInstance(clazz, "");
+ }
+
+
+ @Test
+ public void require_that_arrays_config_is_resolved() throws ParserConfigurationException, IOException, SAXException {
+ Element configRoot = getDocument("<config name=\"arraytypes\">" +
+ " <intarr operation=\"append\">13</intarr>" +
+ " <intarr operation=\"append\">10</intarr>" +
+ " <intarr operation=\"append\">1337</intarr>" +
+ "</config>");
+ UserConfigRepo map = UserConfigBuilder.build(configRoot, configDefinitionStore, new BaseDeployLogger());
+ assertFalse(map.isEmpty());
+ ConfigDefinitionKey key = new ConfigDefinitionKey("arraytypes", "config");
+ assertNotNull(map.get(key));
+ ArraytypesConfig config = createConfig(ArraytypesConfig.class, map.get(key));
+ assertThat(config.intarr().size(), is(3));
+ assertThat(config.intarr(0), is(13));
+ assertThat(config.intarr(1), is(10));
+ assertThat(config.intarr(2), is(1337));
+ }
+
+ @Test
+ public void require_that_arrays_of_structs_are_resolved() throws ParserConfigurationException, IOException, SAXException {
+ Element configRoot = getDocument(
+ " <config name='vespa.configdefinition.specialtokens'>" +
+ " <tokenlist operation='append'>" +
+ " <name>default</name>" +
+ " <tokens operation='append'>" +
+ " <token>dvd+-r</token>" +
+ " </tokens>" +
+ " </tokenlist>" +
+ " </config>"
+ );
+ assertArraysOfStructs(configRoot);
+ }
+
+ private void assertArraysOfStructs(Element configRoot) {
+ UserConfigRepo map = UserConfigBuilder.build(configRoot, configDefinitionStore, new BaseDeployLogger());
+ assertFalse(map.isEmpty());
+ ConfigDefinitionKey key = new ConfigDefinitionKey(SpecialtokensConfig.CONFIG_DEF_NAME, SpecialtokensConfig.CONFIG_DEF_NAMESPACE);
+ assertNotNull(map.get(key));
+ SpecialtokensConfig config = createConfig(SpecialtokensConfig.class, map.get(key));
+ assertThat(config.tokenlist().size(), is(1));
+ assertThat(config.tokenlist().get(0).name(), is("default"));
+ assertThat(config.tokenlist().get(0).tokens().size(), is(1));
+ assertThat(config.tokenlist().get(0).tokens().get(0).token(), is("dvd+-r"));
+ }
+
+ @Test
+ public void no_exception_when_config_class_does_not_exist() throws ParserConfigurationException, IOException, SAXException {
+ Element configRoot = getDocument("<config name=\"unknown\">" +
+ " <foo>1</foo>" +
+ "</config>");
+ UserConfigRepo repo = UserConfigBuilder.build(configRoot, configDefinitionStore, new BaseDeployLogger());
+ ConfigPayloadBuilder builder = repo.get(new ConfigDefinitionKey("unknown", "config"));
+ assertNotNull(builder);
+ }
+
+ private Element getDocument(String xml) throws ParserConfigurationException {
+ Reader xmlReader = new StringReader("<model>" + xml + "</model>");
+ Document doc;
+ try {
+ doc = XmlHelper.getDocumentBuilder().parse(new InputSource(xmlReader));
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ return doc.getDocumentElement();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/Bug6068056Test.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/Bug6068056Test.java
new file mode 100644
index 00000000000..beb408324c5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/Bug6068056Test.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class Bug6068056Test {
+ private final static String HOSTS = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<hosts>" +
+ " <host name=\"localhost\">" +
+ " <alias>node1</alias>" +
+ " </host>" +
+ "</hosts>";
+
+ private final static String SERVICES = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services>" +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node1\" />" +
+ " </admin>" +
+
+ " <jdisc id=\"docproc\" version=\"1.0\">" +
+ " <search/>" +
+ " <document-processing/>" +
+ " <nodes>" +
+ " <node hostalias=\"node1\"/>" +
+ " </nodes>" +
+ " </jdisc>" +
+
+ "<content version='1.0' id='music'>\n" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents/>\n" +
+ " <group name='mygroup'>\n" +
+ " <node hostalias='node1' distribution-key='0'/>\n" +
+ " </group>\n" +
+ " <engine>\n" +
+ " <proton>\n" +
+ " <searchable-copies>1</searchable-copies>\n" +
+ " </proton>\n" +
+ " </engine>\n" +
+ " </content>" +
+ "</services>";
+
+ @Test(expected = RuntimeException.class)
+ public void testContainerClusterCalledDocproc() throws Exception {
+ VespaModelCreatorWithMockPkg creator = new VespaModelCreatorWithMockPkg(HOSTS, SERVICES);
+ creator.create();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2BuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2BuilderTest.java
new file mode 100755
index 00000000000..98c27098c3d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminV2BuilderTest.java
@@ -0,0 +1,214 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.config.model.api.ConfigServerSpec;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.admin.*;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class DomAdminV2BuilderTest extends DomBuilderTest {
+
+ private static MockRoot root;
+
+ @Before
+ public void prepareTest() throws Exception {
+ root = new MockRoot("root");
+ }
+
+ // Supported for backwards compatibility
+ private Element servicesConfigserver() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <configserver hostalias=\"mockhost\"/>" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ "</admin>").getDocumentElement();
+
+ }
+
+ private Element servicesOverride() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ " <config name=\"cloud.config.log.logd\">" +
+ " <logserver><host>foobar</host></logserver>" +
+ " </config>" +
+ "</admin>").getDocumentElement();
+
+ }
+
+ private Element servicesConfigservers() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <configservers>" +
+ " <configserver hostalias=\"mockhost\"/>" +
+ " </configservers>" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ "</admin>").getDocumentElement();
+ }
+
+ private Element servicesYamas() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <configservers>" +
+ " <configserver hostalias=\"mockhost\"/>" +
+ " </configservers>" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ " <yamas systemname=\"foo\"/>" +
+ "</admin>").getDocumentElement();
+ }
+
+ private Element servicesNoYamas() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <configservers>" +
+ " <configserver hostalias=\"mockhost\"/>" +
+ " </configservers>" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ "</admin>").getDocumentElement();
+ }
+
+ private Element servicesAdminServerOnly() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ "</admin>").getDocumentElement();
+ }
+
+ private Element servicesYamasIntervalOverride() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <configservers>" +
+ " <configserver hostalias=\"mockhost\"/>" +
+ " </configservers>" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ " <yamas systemname=\"foo\" interval=\"300\"/>" +
+ "</admin>").getDocumentElement();
+ }
+
+ private Element servicesMultitenantAdminOnly() {
+ return XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <adminserver hostalias=\"mockhost\" />" +
+ "</admin>").getDocumentElement();
+ }
+
+ @Test
+ public void multitenant() {
+ List<ConfigServerSpec> configServerSpecs = Arrays.asList(
+ new Configserver.Spec("test1", 19070, 19071, 2181),
+ new Configserver.Spec("test2", 19070, 19071, 2181),
+ new Configserver.Spec("test3", 19070, 19071, 2181));
+ Admin admin = buildAdmin(servicesMultitenantAdminOnly(), true, configServerSpecs);
+ assertThat(admin.getConfigservers().size(), is(3));
+ assertThat(admin.getSlobroks().size(), is(1));
+ assertThat(admin.getClusterControllerHosts().size(), is(1));
+ assertNotNull(admin.getHostSystem().getHostByHostname("test1"));
+ for (Configserver configserver : admin.getConfigservers()) {
+ assertThat(configserver.getHostName(), is(not(admin.getClusterControllerHosts().get(0).getHost().getHostName())));
+ for (Slobrok slobrok : admin.getSlobroks()) {
+ assertThat(slobrok.getHostName(), is(not(configserver.getHostName())));
+ }
+ }
+ }
+
+ /**
+ * Tests that configserver works (deprecated, but allowed in admin 2.0)
+ */
+ @Test
+ public void adminWithConfigserverElement() {
+ Admin admin = buildAdmin(servicesConfigserver());
+ assertThat(admin.getConfigservers().size(), is(1));
+ }
+
+ /**
+ * Tests that configservers/configserver works
+ */
+ @Test
+ public void adminWithConfigserversElement() {
+ Admin admin = buildAdmin(servicesConfigservers());
+ assertThat(admin.getConfigservers().size(), is(1));
+ }
+
+ @Test
+ public void basicYamasNoXml() {
+ Admin admin = buildAdmin(servicesNoYamas());
+ Yamas y = admin.getYamas();
+ assertThat(y.getClustername(), is("vespa"));
+ assertThat(y.getInterval(), is(1));
+ }
+
+ @Test
+ public void testAdminServerOnly() {
+ Admin admin = buildAdmin(servicesAdminServerOnly());
+ assertEquals(1, admin.getSlobroks().size());
+ }
+
+ @Test
+ public void basicYamasXml() {
+ Admin admin = buildAdmin(servicesYamas());
+ Yamas y = admin.getYamas();
+ assertThat(y.getClustername(), is("foo"));
+ assertThat(y.getInterval(), is(1));
+ }
+
+ @Test
+ public void yamasWithIntervalOverride() {
+ Admin admin = buildAdmin(servicesYamasIntervalOverride());
+ Yamas y = admin.getYamas();
+ assertThat(y.getClustername(), is("foo"));
+ assertThat(y.getInterval(), is(5));
+ }
+
+ /**
+ * Test that illegal yamas interval throws exception
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void yamasElementInvalid() {
+ Element servicesYamasIllegalInterval = XML.getDocument(
+ "<admin version=\"2.0\">" +
+ " <adminserver hostalias=\"mockhost\"/>" +
+ " <yamas interval=\"5\"/>" +
+ "</admin>").getDocumentElement();
+ Admin admin = buildAdmin(servicesYamasIllegalInterval);
+ }
+
+ @Test
+ public void configOverridesCanBeUsedInAdmin() {
+ Admin admin = buildAdmin(servicesOverride());
+ assertThat(admin.getUserConfigs().size(), is(1));
+ LogdConfig.Builder logdBuilder = new LogdConfig.Builder();
+ admin.addUserConfig(logdBuilder);
+ LogdConfig config = new LogdConfig(logdBuilder);
+ assertThat(config.logserver().host(), is("foobar"));
+ }
+
+ private Admin buildAdmin(Element xml) {
+ return buildAdmin(xml, false, new ArrayList<>());
+ }
+
+ private Admin buildAdmin(Element xml, boolean multitenant, List<ConfigServerSpec> configServerSpecs) {
+ final DomAdminV2Builder domAdminBuilder = new DomAdminV2Builder(root.getDeployState().getFileRegistry(), multitenant, configServerSpecs);
+ Admin admin = domAdminBuilder.build(root, xml);
+ admin.addPerHostServices(root.getHostSystem().getHosts(), new DeployProperties.Builder().build());
+ return admin;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilderTest.java
new file mode 100644
index 00000000000..c224c81fc34
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomComponentBuilderTest.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.vespa.model.container.component.Component;
+import org.junit.Test;
+
+import static com.yahoo.collections.CollectionUtil.first;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ */
+public class DomComponentBuilderTest extends DomBuilderTest {
+
+ @Test
+ public void ensureCorrectModel() {
+ Component<?, ?> handler = new DomComponentBuilder().doBuild(root, parse(
+ "<handler id='theId' class='theClass' bundle='theBundle' />"));
+
+ BundleInstantiationSpecification instantiationSpecification = handler.model.bundleInstantiationSpec;
+ assertThat(instantiationSpecification.id.stringValue(), is("theId"));
+ assertThat(instantiationSpecification.classId.stringValue(), is("theClass"));
+ assertThat(instantiationSpecification.bundle.stringValue(), is("theBundle"));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void components_can_be_nested() {
+ Component<Component<?, ?>, ?> parent = new DomComponentBuilder().doBuild(root, parse(
+ "<component id='parent'>",
+ " <component id='child' />",
+ "</component>"));
+
+ assertThat(parent.getGlobalComponentId(), is(ComponentId.fromString("parent")));
+ Component<?, ?> child = first(parent.getChildren().values());
+ assertNotNull(child);
+
+ assertThat(child.getGlobalComponentId(), is(ComponentId.fromString("child@parent")));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilderTest.java
new file mode 100644
index 00000000000..547b95357c5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomConfigPayloadBuilderTest.java
@@ -0,0 +1,326 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.model.builder.xml.XmlHelper;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionBuilder;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.ConfigPayloadBuilder;
+
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.*;
+import java.util.ArrayList;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for the {@link com.yahoo.vespa.model.builder.xml.dom.DomConfigPayloadBuilder} class.
+ *
+ * @author gjoranv
+ * @author lulf
+ */
+public class DomConfigPayloadBuilderTest {
+
+ @Test
+ public void testFunctionTest_DefaultValues() throws FileNotFoundException, ParserConfigurationException {
+ Element configRoot = getDocument(new FileReader(new File("src/test/cfg/admin/userconfigs/functiontest-defaultvalues.xml")));
+ ConfigPayload config = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<>()));
+ String expected = ""
+ + "{"
+ + "\"bool_val\":\"false\","
+ + "\"int_val\":\"5\","
+ + "\"long_val\":\"1234567890123\","
+ + "\"double_val\":\"41.23\","
+ + "\"string_val\":\"foo\","
+ + "\"enum_val\":\"FOOBAR\","
+ + "\"refval\":\":parent:\","
+ + "\"fileVal\":\"vespa.log\","
+ + "\"basicStruct\":{\"bar\":\"3\",\"intArr\":[\"10\"]},"
+ + "\"rootStruct\":{\"inner0\":{\"index\":\"11\"},\"inner1\":{\"index\":\"12\"},"
+ + "\"innerArr\":[{\"stringVal\":\"deep\"}]},"
+ + "\"boolarr\":[\"false\"],"
+ + "\"doublearr\":[\"2344\",\"123\"],"
+ + "\"stringarr\":[\"bar\"],"
+ + "\"enumarr\":[\"VALUES\"],"
+ + "\"myarray\":[{\"refval\":\":parent:\",\"fileVal\":\"command.com\",\"myStruct\":{\"a\":\"1\"},\"stringval\":[\"baah\",\"yikes\"],\"anotherarray\":[{\"foo\":\"7\"}]},{\"refval\":\":parent:\",\"fileVal\":\"display.sys\",\"myStruct\":{\"a\":\"-1\"},\"anotherarray\":[{\"foo\":\"1\"},{\"foo\":\"2\"}]}]"
+ + "}";
+ assertPayload(expected, config);
+ }
+
+ private void assertPayload(String expected, ConfigPayload payload) {
+ try {
+ ByteArrayOutputStream a = new ByteArrayOutputStream();
+ new JsonFormat(true).encode(a, payload.getSlime());
+ assertThat(a.toString(), is(expected));
+ } catch (Exception e) {
+ fail("Exception thrown when encoding slime: " + e.getMessage());
+ }
+
+ }
+ // Multi line strings are not tested in 'DefaultValues', so here it is.
+ @Test
+ public void verifyThatWhitespaceIsPreservedForStrings() throws Exception {
+ Element configRoot = getDocument(new FileReader(new File("src/test/cfg/admin/userconfigs/whitespace-test.xml")));
+ ConfigPayload config = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>()));
+ assertPayload("{\"stringVal\":\" This is a string\\n that contains different kinds of whitespace \"}", config);
+ }
+
+ @Test
+ public void put_to_leaf_map() throws Exception {
+ Reader xmlConfig = new StringReader("<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<config name=\"foobar\">" +
+ " <intmap>" +
+ " <item key=\"bar\">1338</item>" +
+ " <item key=\"foo\">1337</item>" +
+ " </intmap>" +
+ "</config>");
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(getDocument(xmlConfig), new ArrayList<String>()));
+ assertPayload("{\"intmap\":{\"bar\":\"1338\",\"foo\":\"1337\"}}", userConfig);
+ }
+
+ @Test
+ public void put_to_inner_map() throws Exception {
+ Reader xmlConfig = new StringReader("<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<config name=\"foobar\">" +
+ " <innermap>" +
+ " <item key=\"bar\">" +
+ " <foo>baz</foo>" +
+ " </item>" +
+ " <item key=\"foo\">" +
+ " <foo>bar</foo>" +
+ " </item>" +
+ " </innermap>" +
+ "</config>");
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(getDocument(xmlConfig), new ArrayList<String>()));
+ assertPayload("{\"innermap\":{\"bar\":{\"foo\":\"baz\"},\"foo\":{\"foo\":\"bar\"}}}", userConfig);
+ }
+
+ @Test
+ public void put_to_nested_map() throws Exception {
+ Reader xmlConfig = new StringReader("<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<config name=\"foobar\">" +
+ " <nestedmap>" +
+ " <item key=\"bar\">" +
+ " <inner>" +
+ " <item key=\"bar1\">30</item>" +
+ " <item key=\"bar2\">40</item>" +
+ " </inner>" +
+ " </item>" +
+ " <item key=\"foo\">" +
+ " <inner>" +
+ " <item key=\"foo1\">10</item>" +
+ " <item key=\"foo2\">20</item>" +
+ " </inner>" +
+ " </item>" +
+ " </nestedmap>" +
+ "</config>");
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(getDocument(xmlConfig), new ArrayList<String>()));
+ assertPayload("{\"nestedmap\":{" +
+ "\"bar\":{\"inner\":{\"bar1\":\"30\",\"bar2\":\"40\"}}," +
+ "\"foo\":{\"inner\":{\"foo1\":\"10\",\"foo2\":\"20\"}}}}", userConfig);
+ }
+
+ @Test
+ public void append_to_leaf_array() throws Exception {
+ // Simulate user config from vespa-services.xml
+ Reader xmlConfig = new StringReader("<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<config name=\"function-test\">" +
+ " <intarr operation=\"append\">1</intarr>" +
+ " <intarr operation=\"append\">2</intarr>" +
+ "</config> ");
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(getDocument(xmlConfig), new ArrayList<String>()));
+ assertPayload("{\"intarr\":[\"1\",\"2\"]}", userConfig);
+ }
+
+ @Test
+ public void camel_case_via_dashes() throws Exception {
+ Reader xmlConfig = new StringReader("<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<config name=\"function-test\">" +
+ " <some-struct> <any-value>17</any-value> </some-struct>" +
+ "</config> ");
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(getDocument(xmlConfig), new ArrayList<String>()));
+ assertPayload("{\"someStruct\":{\"anyValue\":\"17\"}}", userConfig);
+ }
+
+ // Verifies that an exception is thrown when the root element is not 'config'.
+ @Test
+ public void testFailWrongTagName() throws FileNotFoundException, ParserConfigurationException {
+ Element configRoot = getDocument(new StringReader("<configs name=\"foo\"/>"));
+ try {
+ new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>());
+ fail("Expected exception for wrong tag name.");
+ } catch (ConfigurationRuntimeException e) {
+ assertThat(e.getMessage(),
+ is("The root element must be 'config', but was 'configs'."));
+ }
+ }
+
+ // Verifies that an exception is thrown when the root element is not 'config'.
+ @Test
+ public void testFailNoNameAttribute() throws FileNotFoundException, ParserConfigurationException {
+ Element configRoot = getDocument(new StringReader("<config/>"));
+ try {
+ new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>());
+ fail("Expected exception for mismatch between def-name and xml name attribute.");
+ } catch (ConfigurationRuntimeException e) {
+ assertThat(e.getMessage(),
+ is("The 'config' element must have a 'name' attribute that matches the name of the config definition."));
+ }
+ }
+
+ @Test
+ public void testNamespace() throws FileNotFoundException, ParserConfigurationException {
+ Element configRoot = getDocument(new StringReader("<config name=\"function-test\" namespace=\"config\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ ConfigPayload config = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>()));
+ assertPayload("{\"int_val\":\"1\"}", config);
+
+ configRoot = getDocument(new StringReader("<config name=\"config.function-test\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ config = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>()));
+ assertPayload("{\"int_val\":\"1\"}", config);
+
+ configRoot = getDocument(new StringReader("<config name=\"config.function_test\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ config = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>()));
+ assertPayload("{\"int_val\":\"1\"}", config);
+ }
+
+ @Test
+ public void testNameParsing() throws FileNotFoundException, ParserConfigurationException {
+ Element configRoot = getDocument(new StringReader("<config name=\"function-test\" version=\"1\" namespace=\"config\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ ConfigDefinitionKey key = DomConfigPayloadBuilder.parseConfigName(configRoot);
+ assertThat(key.getName(), is("function-test"));
+ assertThat(key.getNamespace(), is("config"));
+
+ configRoot = getDocument(new StringReader("<config name=\"function_test\" version=\"1\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ key = DomConfigPayloadBuilder.parseConfigName(configRoot);
+ assertThat(key.getName(), is("function_test"));
+ assertThat(key.getNamespace(), is("config"));
+
+ // Both namespace and name in name attribute
+ configRoot = getDocument(new StringReader("<config name=\"config.function-test\" version=\"1\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ key = DomConfigPayloadBuilder.parseConfigName(configRoot);
+ assertThat(key.getName(), is("function-test"));
+ assertThat(key.getNamespace(), is("config"));
+ }
+
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testNameParsingInvalidName() throws FileNotFoundException, ParserConfigurationException {
+ Element configRoot = getDocument(new StringReader("<config name=\" function-test\" version=\"1\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ DomConfigPayloadBuilder.parseConfigName(configRoot);
+ }
+
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void testNameParsingInvalidNamespace() throws FileNotFoundException, ParserConfigurationException {
+ Element configRoot = getDocument(new StringReader("<config name=\"function-test\" namespace=\"_foo\" version=\"1\">" +
+ "<int_val>1</int_val> +" +
+ "</config>"));
+ DomConfigPayloadBuilder.parseConfigName(configRoot);
+ }
+
+ @Test
+ public void require_that_item_syntax_works_with_leaf() throws ParserConfigurationException {
+ Element configRoot = getDocument(
+ "<config name=\"arraytypes\" version=\"1\">" +
+ " <intarr>" +
+ " <item>13</item>" +
+ " <item>10</item>" +
+ " <item>1337</item>" +
+ " </intarr>" +
+ "</config>");
+
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>()));
+ assertPayload("{\"intarr\":[\"13\",\"10\",\"1337\"]}", userConfig);
+ }
+
+ @Test
+ public void require_that_item_syntax_works_with_struct() throws ParserConfigurationException {
+ Element configRoot = getDocument(
+ "<config name=\"arraytypes\" version=\"1\">" +
+ " <lolarray>" +
+ " <item><foo>hei</foo><bar>hei2</bar></item>" +
+ " <item><foo>hoo</foo><bar>hoo2</bar></item>" +
+ " <item><foo>happ</foo><bar>happ2</bar></item>" +
+ " </lolarray>" +
+ "</config>");
+
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>()));
+ assertPayload("{\"lolarray\":[{\"foo\":\"hei\",\"bar\":\"hei2\"},{\"foo\":\"hoo\",\"bar\":\"hoo2\"},{\"foo\":\"happ\",\"bar\":\"happ2\"}]}",
+ userConfig);
+ }
+
+ @Test
+ public void require_that_item_syntax_works_with_struct_array() throws ParserConfigurationException {
+ Element configRoot = getDocument(
+ "<config name=\"arraytypes\" version=\"1\">" +
+ " <lolarray>" +
+ " <item><fooarray><item>13</item></fooarray></item>" +
+ " <item><fooarray><item>10</item></fooarray></item>" +
+ " <item><fooarray><item>1337</item></fooarray></item>" +
+ " </lolarray>" +
+ "</config>");
+
+ ConfigPayload userConfig = ConfigPayload.fromBuilder(new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>()));
+ assertPayload("{\"lolarray\":[{\"fooarray\":[\"13\"]},{\"fooarray\":[\"10\"]},{\"fooarray\":[\"1337\"]}]}", userConfig);
+ }
+
+ @Test(expected = ConfigurationRuntimeException.class)
+ public void require_that_item_is_reserved_in_root() throws ParserConfigurationException {
+ Element configRoot = getDocument(
+ "<config name=\"arraytypes\" version=\"1\">" +
+ " <item>13</item>" +
+ "</config>");
+ new DomConfigPayloadBuilder(null).build(configRoot, new ArrayList<String>());
+ }
+
+ @Test(expected=ConfigurationRuntimeException.class)
+ public void require_that_exceptions_are_issued() throws ParserConfigurationException, FileNotFoundException {
+ Element configRoot = getDocument(
+ "<config name=\"simpletypes\">" +
+ "<longval>invalid</longval>" +
+ "</config>");
+ DefParser defParser = new DefParser("simpletypes",
+ new FileReader(new File("src/test/resources/configdefinitions/simpletypes.def")));
+ ConfigDefinition def = ConfigDefinitionBuilder.createConfigDefinition(defParser.getTree());
+ ConfigPayloadBuilder builder = new DomConfigPayloadBuilder(def).build(configRoot, new ArrayList<String>());
+ //assertThat(builder.warnings().size(), is(1));
+ }
+
+ private Element getDocument(Reader xmlReader) throws ParserConfigurationException {
+ Document doc;
+ try {
+ doc = XmlHelper.getDocumentBuilder().parse(new InputSource(xmlReader));
+ } catch (Exception e) {
+ throw new RuntimeException();
+ }
+ return doc.getDocumentElement();
+ }
+
+ private Element getDocument(String xml) throws ParserConfigurationException {
+ Reader xmlReader = new StringReader(xml);
+ return getDocument(xmlReader);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilderTest.java
new file mode 100644
index 00000000000..60d9fce767e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomContentBuilderTest.java
@@ -0,0 +1,817 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.collections.CollectionUtil;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigPayloadBuilder;
+import com.yahoo.vespa.config.GenericConfig;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.Service;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.content.ContentSearchCluster;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.engines.ProtonEngine;
+import com.yahoo.vespa.model.content.engines.VDSEngine;
+import com.yahoo.vespa.model.search.*;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.*;
+
+/**
+ * @author balder
+ */
+public class DomContentBuilderTest extends DomBuilderTest {
+ private ContentCluster createContent(String xml) throws Exception {
+ String combined = "" +
+ "<services>"+
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='mockhost'/>" +
+ " </admin>" +
+ xml +
+ "</services>";
+
+
+ VespaModel m = new VespaModelCreatorWithMockPkg(new MockApplicationPackage.Builder()
+ .withHosts(getHosts())
+ .withServices(combined)
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build())
+ .create();
+
+ return m.getContentClusters().isEmpty()
+ ? null
+ : m.getContentClusters().values().iterator().next();
+ }
+ private ContentCluster createContentWithBooksToo(String xml) throws Exception {
+ String combined = "" +
+ "<services>"+
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='mockhost'/>" +
+ " </admin>" +
+ xml +
+ "</services>";
+
+ VespaModel m = new VespaModelCreatorWithMockPkg(new MockApplicationPackage.Builder()
+ .withHosts(getHosts())
+ .withServices(combined)
+ .withSearchDefinitions(Arrays.asList(MockApplicationPackage.MUSIC_SEARCHDEFINITION,
+ MockApplicationPackage.BOOK_SEARCHDEFINITION))
+ .build())
+ .create();
+
+ return m.getContentClusters().isEmpty()
+ ? null
+ : m.getContentClusters().values().iterator().next();
+ }
+
+ private String getHosts() {
+ return "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<hosts>" +
+ " <host name='node0'>" +
+ " <alias>mockhost</alias>" +
+ " </host>" +
+ " <host name='node1'>" +
+ " <alias>mockhost2</alias>" +
+ " </host>" +
+ " <host name='node2'>" +
+ " <alias>mockhost3</alias>" +
+ " </host>" +
+ "</hosts>";
+ }
+
+ private String getServices(String groupXml) {
+ return getConfigOverrideServices(groupXml, "");
+ }
+
+ private String getConfigOverrideServices(String groupXml, String documentOverrides) {
+ return "" +
+ "<services>"+
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='mockhost'/>" +
+ " </admin>" +
+ " <jdisc version='1.0' id='qrc'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='mockhost' />" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content version='1.0' id='clu'>" +
+ " <documents>" +
+ " <document type='music' mode='index'>" +
+ documentOverrides +
+ " </document>" +
+ " </documents>" +
+ " <redundancy>3</redundancy>"+
+ " <engine>" +
+ " <proton>" +
+ " <query-timeout>7.3</query-timeout>" +
+ " </proton>" +
+ " </engine>" +
+ " <group>"+
+ groupXml +
+ " </group>"+
+ " </content>" +
+ "</services>";
+ }
+
+ private String getBasicServices() {
+ return getServices("<node hostalias='mockhost' distribution-key='0'/>");
+ }
+
+ public static void assertServices(HostResource host, String [] services) {
+ String missing = "";
+
+ for (String s : services) {
+ if (host.getService(s) == null) {
+ missing += s + ",";
+ }
+ }
+
+ String extra = "";
+ for (Service s : host.getServices()) {
+ boolean found = false;
+ for (String n : services) {
+ if (n.equals(s.getServiceName())) {
+ found = true;
+ }
+ }
+
+ if (!found) {
+ extra += s.getServiceName() + ",";
+ }
+ }
+
+ assertEquals("Missing: Extra: ", "Missing: " + missing+ " Extra: " + extra);
+
+ assertEquals(services.length, host.getServices().size());
+ }
+
+ @Test
+ public void handleSingleNonSearchPersistentDummy() throws Exception {
+ ContentCluster a = createContent(
+ "<content version =\"1.0\" id=\"a\">"+
+ " <redundancy>3</redundancy>"+
+ " <documents>" +
+ " <document type=\"music\" mode=\"store-only\"/>" +
+ " </documents>" +
+ " <engine>"+
+ " <dummy/>"+
+ " </engine>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ "</content>");
+
+ ContentSearchCluster s = a.getSearch();
+ assertFalse(s.hasIndexedCluster());
+ assertTrue(s.getClusters().isEmpty());
+
+ assertTrue(a.getPersistence() instanceof com.yahoo.vespa.model.content.engines.DummyPersistence.Factory);
+ }
+
+ @Test
+ public void handleSingleNonSearchPersistentVds() throws Exception {
+ ContentCluster a = createContent(
+ "<content version =\"1.0\" id=\"a\">"+
+ " <redundancy>3</redundancy>"+
+ " <documents>" +
+ " <document type=\"music\" mode=\"store-only\"/>" +
+ " </documents>" +
+ " <engine>"+
+ " <vds/>"+
+ " </engine>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ "</content>");
+
+ ContentSearchCluster s = a.getSearch();
+ assertFalse(s.hasIndexedCluster());
+ assertTrue(s.getClusters().isEmpty());
+
+ assertTrue(a.getPersistence() instanceof VDSEngine.Factory);
+
+ assertEquals(1, a.getStorageNodes().getChildren().size());
+ }
+
+ @Test
+ public void handleSingleNonSearchPersistentProton() throws Exception {
+ ContentCluster a = createContent(
+ "<content version =\"1.0\" id=\"a\">"+
+ " <redundancy>3</redundancy>"+
+ " <documents>" +
+ " <document type=\"music\" mode=\"store-only\"/>" +
+ " </documents>" +
+ " <engine>"+
+ " <proton/>"+
+ " </engine>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ "</content>");
+
+ ContentSearchCluster s = a.getSearch();
+ assertFalse(s.hasIndexedCluster());
+ assertTrue(s.getClusters().isEmpty());
+
+ assertTrue(a.getPersistence() instanceof ProtonEngine.Factory);
+
+ assertEquals(1, a.getStorageNodes().getChildren().size());
+ }
+
+ @Test
+ public void handleSingleNonSearchNonPersistentCluster() throws Exception {
+ ContentCluster a = createContent(
+ "<content version =\"1.0\" id=\"a\">"+
+ " <redundancy>3</redundancy>"+
+ " <documents>" +
+ " <document type=\"music\" mode=\"store-only\"/>" +
+ " </documents>" +
+ " <engine>"+
+ " <vds/>"+
+ " </engine>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ "</content>");
+
+ ContentSearchCluster s = a.getSearch();
+ assertFalse(s.hasIndexedCluster());
+ assertTrue(s.getClusters().isEmpty());
+ assertNull(s.getIndexed());
+
+ assertNull(a.getRootGroup().getName());
+ assertNull(a.getRootGroup().getIndex());
+ assertTrue(a.getRootGroup().getSubgroups().isEmpty());
+ assertEquals(1, a.getRootGroup().getNodes().size());
+ assertEquals("node0", a.getRootGroup().getNodes().get(0).getHostName());
+
+ assertTrue(a.getPersistence() instanceof VDSEngine.Factory);
+ assertEquals(1, a.getStorageNodes().getChildren().size());
+ assertEquals("a", a.getConfigId());
+ }
+
+ @Test
+ public void handleIndexedOnlyWithoutPersistence() throws Exception {
+ VespaModel m = new VespaModelCreatorWithMockPkg(createAppWithMusic(getHosts(), getBasicServices())).create();
+
+ ContentCluster c = CollectionUtil.first(m.getContentClusters().values());
+ ContentSearchCluster s = c.getSearch();
+ assertTrue(s.hasIndexedCluster());
+ assertEquals(1, s.getClusters().size());
+ assertNotNull(s.getIndexed());
+ assertEquals("clu", s.getIndexed().getClusterName());
+ assertEquals(7.3, s.getIndexed().getQueryTimeout(), 0.0);
+
+ assertTrue(c.getPersistence() instanceof ProtonEngine.Factory);
+ assertEquals(1, c.getStorageNodes().getChildren().size());
+ assertEquals("clu", c.getConfigId());
+ //assertEquals("content/a/0", a.getRootGroup().getNodes().get(0).getConfigId()); // This is how it should look like in an ideal world.
+ assertEquals("clu/storage/0", c.getRootGroup().getNodes().get(0).getConfigId()); // Due to reuse.
+ assertEquals(1, c.getRoot().getHostSystem().getHosts().size());
+ HostResource h = c.getRoot().getHostSystem().getHost("mockhost");
+ String [] expectedServices = {"logd", "configproxy","config-sentinel", "qrserver", "storagenode", "searchnode", "distributor", "topleveldispatch", "transactionlogserver"};
+// TODO assertServices(h, expectedServices);
+ assertEquals("clu/storage/0", h.getService("storagenode").getConfigId());
+ assertEquals("clu/search/cluster.clu/0", h.getService("searchnode").getConfigId());
+ assertEquals("clu/distributor/0", h.getService("distributor").getConfigId());
+ assertEquals("clu/search/cluster.clu/tlds/qrc.0.tld.0", h.getService("topleveldispatch").getConfigId());
+ //assertEquals("tcp/node0:19104", h.getService("topleveldispatch").getConfig("partitions", "").innerArray("dataset").value("0").innerArray("engine").value("0").getString("name_and_port"));
+ PartitionsConfig partitionsConfig = new PartitionsConfig((PartitionsConfig.Builder)
+ m.getConfig(new PartitionsConfig.Builder(), "clu/search/cluster.clu/tlds/qrc.0.tld.0"));
+ assertTrue(partitionsConfig.dataset(0).engine(0).name_and_port().startsWith("tcp/node0:191"));
+ }
+
+ @Test
+ public void testConfigIdLookup() throws Exception {
+ VespaModel m = new VespaModelCreatorWithMockPkg(createAppWithMusic(getHosts(), getBasicServices())).create();
+
+ PartitionsConfig partitionsConfig = new PartitionsConfig((PartitionsConfig.Builder)
+ m.getConfig(new PartitionsConfig.Builder(), "clu/search/cluster.clu/tlds/qrc.0.tld.0"));
+ assertTrue(partitionsConfig.dataset(0).engine(0).name_and_port().startsWith("tcp/node0:191"));
+ }
+
+ @Test
+ public void testMultipleSearchNodesOnSameHost() throws Exception {
+ String services = getServices("<node hostalias='mockhost' distribution-key='0'/>" +
+ "<node hostalias='mockhost' distribution-key='1'/>");
+ VespaModel m = new VespaModelCreatorWithMockPkg(createAppWithMusic(getHosts(), services)).create();
+ PartitionsConfig partitionsConfig = new PartitionsConfig((PartitionsConfig.Builder)
+ m.getConfig(new PartitionsConfig.Builder(), "clu/search/cluster.clu/tlds/qrc.0.tld.0"));
+ assertTrue(partitionsConfig.dataset(0).engine(0).name_and_port().startsWith("tcp/node0:191"));
+ IndexedSearchCluster sc = m.getContentClusters().get("clu").getSearch().getIndexed();
+ assertEquals(2, sc.getSearchNodeCount());
+ assertTrue(sc.getSearchNode(0).getPersistenceProviderRpcPort() >= 19100);
+ assertTrue(sc.getSearchNode(0).getPersistenceProviderRpcPort() != sc.getSearchNode(1).getPersistenceProviderRpcPort());
+ }
+
+ @Test
+ public void handleStreamingOnlyWithoutPersistence() throws Exception
+ {
+ final String musicClusterId = "music-cluster-id";
+
+ ContentCluster cluster = createContent(
+ "<content version='1.0' id='" + musicClusterId + "'>" +
+ " <redundancy>3</redundancy>"+
+ " <documents>"+
+ " <document type='music' mode='streaming'/>"+
+ " </documents>"+
+ " <engine>"+
+ " <vds/>"+
+ " </engine>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ "</content>");
+ ContentSearchCluster s;
+
+ s = cluster.getSearch();
+ assertFalse(s.hasIndexedCluster());
+ assertEquals(1, s.getClusters().size());
+ assertNull(s.getIndexed());
+ AbstractSearchCluster sc = s.getClusters().get(musicClusterId + ".music");
+ assertEquals(musicClusterId + ".music", sc.getClusterName());
+ assertEquals(musicClusterId, ((StreamingSearchCluster)sc).getStorageRouteSpec());
+
+ assertTrue(cluster.getPersistence() instanceof VDSEngine.Factory);
+ assertEquals(1, cluster.getStorageNodes().getChildren().size());
+
+ assertEquals(musicClusterId, cluster.getConfigId());
+ //assertEquals("content/a/0", a.getRootGroup().getNodes().get(0).getConfigId());
+ assertEquals(musicClusterId + "/storage/0", cluster.getRootGroup().getNodes().get(0).getConfigId()); // Due to reuse.
+ assertEquals(1, cluster.getRoot().getHostSystem().getHosts().size());
+ HostResource h = cluster.getRoot().getHostSystem().getHost("mockhost");
+ String [] expectedServices = {
+ "logd", "configproxy",
+ "config-sentinel", "configserver", "logserver",
+ "slobrok", "container-clustercontroller",
+ "filedistributorservice", "storagenode", "distributor"
+ };
+ assertServices(h, expectedServices);
+
+ assertEquals(musicClusterId + "/storage/0", h.getService("storagenode").getConfigId());
+
+ /* Not yet
+ assertNotNull(h.getService("qrserver"));
+ assertNotNull(h.getService("topleveldisptach"));
+ assertNotNull(h.getService("docproc"));
+ */
+
+ }
+
+ @Test
+ public void requireThatContentStreamingHandlesMultipleSearchDefinitions() throws Exception
+ {
+ final String musicClusterId = "music-cluster-id";
+
+ ContentCluster cluster = createContentWithBooksToo(
+ "<content version='1.0' id='" + musicClusterId + "'>" +
+ " <redundancy>3</redundancy>"+
+ " <documents>"+
+ " <document type='music' mode='streaming'/>"+
+ " <document type='book' mode='streaming'/>"+
+ " </documents>"+
+ " <engine>"+
+ " <vds/>"+
+ " </engine>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ "</content>");
+ ContentSearchCluster s;
+
+ s = cluster.getSearch();
+ assertFalse(s.hasIndexedCluster());
+ assertEquals(2, s.getClusters().size());
+ assertNull(s.getIndexed());
+ {
+ String id = musicClusterId + ".book";
+ AbstractSearchCluster sc = s.getClusters().get(id);
+ assertEquals(id, sc.getClusterName());
+ assertEquals(musicClusterId, ((StreamingSearchCluster) sc).getStorageRouteSpec());
+ }
+ {
+ String id = musicClusterId + ".music";
+ AbstractSearchCluster sc = s.getClusters().get(id);
+ assertEquals(id, sc.getClusterName());
+ assertEquals(musicClusterId, ((StreamingSearchCluster) sc).getStorageRouteSpec());
+ }
+
+ assertTrue(cluster.getPersistence() instanceof VDSEngine.Factory);
+ assertEquals(1, cluster.getStorageNodes().getChildren().size());
+
+ assertEquals(musicClusterId, cluster.getConfigId());
+ }
+
+ @Test
+ public void handleIndexedWithoutPersistence() throws Exception
+ {
+ ContentCluster b = createContent(
+ "<content version =\"1.0\" id=\"b\">" +
+ " <redundancy>3</redundancy>"+
+ " <documents>"+
+ " <document type='music' mode='index'/>"+
+ " </documents>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ "</content>");
+ ContentSearchCluster s;
+
+ s = b.getSearch();
+ assertTrue(s.hasIndexedCluster());
+ assertEquals(1, s.getClusters().size());
+ assertNotNull(s.getIndexed());
+ assertEquals("b", s.getIndexed().getClusterName());
+
+ assertTrue(b.getPersistence() instanceof ProtonEngine.Factory);
+ assertEquals(1, b.getStorageNodes().getChildren().size());
+
+ assertEquals("b", b.getConfigId());
+ //assertEquals("content/a/0", a.getRootGroup().getNodes().get(0).getConfigId());
+ assertEquals("b/storage/0", b.getRootGroup().getNodes().get(0).getConfigId()); // Due to reuse.
+ assertEquals(1, b.getRoot().getHostSystem().getHosts().size());
+ HostResource h = b.getRoot().getHostSystem().getHost("mockhost");
+ assertEquals("b/storage/0", h.getService("storagenode").getConfigId());
+ }
+
+ @Test
+ public void canConfigureMmapNoCoreLimit() throws Exception {
+ ContentCluster b = createContent(
+ "<content version =\"1.0\" id=\"b\">" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <group mmap-nocore-limit=\"200000\">" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\" />" +
+ " <node hostalias=\"mockhost\" distribution-key=\"1\" />" +
+ " </group>" +
+ "</content>");
+ ContentSearchCluster s;
+
+ s = b.getSearch();
+ assertTrue(s.hasIndexedCluster());
+ assertNotNull(s.getIndexed());
+ assertEquals(2, b.getStorageNodes().getChildren().size());
+ assertTrue(b.getRootGroup().getMmapNoCoreLimit().isPresent());
+ assertEquals(200000, b.getRootGroup().getMmapNoCoreLimit().get().longValue());
+
+ assertThat(s.getSearchNodes().size(), is(2));
+ assertEquals(200000, s.getSearchNodes().get(0).getMMapNoCoreLimit());
+ assertEquals(200000, s.getSearchNodes().get(1).getMMapNoCoreLimit());
+ assertEquals("VESPA_MMAP_NOCORE_LIMIT=200000 ", s.getSearchNodes().get(0).getMMapNoCoreEnvVariable());
+ assertEquals("VESPA_MMAP_NOCORE_LIMIT=200000 ", s.getSearchNodes().get(1).getMMapNoCoreEnvVariable());
+ }
+
+ @Test
+ public void canConfigureMmapNoCoreLimitPerHost() throws Exception {
+ ContentCluster b = createContent(
+ "<content version =\"1.0\" id=\"b\">" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <group>" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\" mmap-nocore-limit=\"200000\"/>" +
+ " <node hostalias=\"mockhost\" distribution-key=\"1\" />" +
+ " </group>" +
+ "</content>");
+ ContentSearchCluster s;
+
+ s = b.getSearch();
+ assertTrue(s.hasIndexedCluster());
+ assertNotNull(s.getIndexed());
+ assertEquals(2, b.getStorageNodes().getChildren().size());
+ assertFalse(b.getRootGroup().getMmapNoCoreLimit().isPresent());
+
+ assertThat(s.getSearchNodes().size(), is(2));
+ assertEquals(200000, s.getSearchNodes().get(0).getMMapNoCoreLimit());
+ assertEquals(-1, s.getSearchNodes().get(1).getMMapNoCoreLimit());
+ assertEquals("VESPA_MMAP_NOCORE_LIMIT=200000 ", s.getSearchNodes().get(0).getMMapNoCoreEnvVariable());
+ assertEquals("", s.getSearchNodes().get(1).getMMapNoCoreEnvVariable());
+ }
+
+ @Test
+ public void canConfigureCpuAffinity() throws Exception
+ {
+ ContentCluster b = createContent(
+ "<content version =\"1.0\" id=\"b\">" +
+ " <redundancy>2</redundancy>"+
+ " <documents>"+
+ " <document type='music' mode='index'/>"+
+ " </documents>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\" cpu-socket=\"0\" />"+
+ " <node hostalias=\"mockhost\" distribution-key=\"1\" cpu-socket=\"1\" />"+
+ " </group>"+
+ "</content>");
+ ContentSearchCluster s;
+
+ s = b.getSearch();
+ assertTrue(s.hasIndexedCluster());
+ assertNotNull(s.getIndexed());
+ assertEquals(2, b.getStorageNodes().getChildren().size());
+ assertTrue(b.getStorageNodes().getChildren().get("0").getAffinity().isPresent());
+ assertThat(b.getStorageNodes().getChildren().get("0").getAffinity().get().cpuSocket(), is(0));
+ assertTrue(b.getStorageNodes().getChildren().get("1").getAffinity().isPresent());
+ assertThat(b.getStorageNodes().getChildren().get("1").getAffinity().get().cpuSocket(), is(1));
+
+ assertThat(s.getSearchNodes().size(), is(2));
+ assertTrue(s.getSearchNodes().get(0).getAffinity().isPresent());
+ assertThat(s.getSearchNodes().get(0).getAffinity().get().cpuSocket(), is(0));
+ assertTrue(s.getSearchNodes().get(1).getAffinity().isPresent());
+ assertThat(s.getSearchNodes().get(1).getAffinity().get().cpuSocket(), is(1));
+ }
+
+ @Test
+ public void canConfigureCpuAffinityAutomatically() throws Exception
+ {
+ ContentCluster b = createContent(
+ "<content version =\"1.0\" id=\"b\">" +
+ " <redundancy>2</redundancy>"+
+ " <documents>"+
+ " <document type='music' mode='index'/>"+
+ " </documents>"+
+ " <group cpu-socket-affinity=\"true\">"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\" />"+
+ " <node hostalias=\"mockhost\" distribution-key=\"1\" />"+
+ " <node hostalias=\"mockhost\" distribution-key=\"2\" />"+
+ " <node hostalias=\"mockhost2\" distribution-key=\"3\" />"+
+ " <node hostalias=\"mockhost2\" distribution-key=\"4\" />"+
+ " <node hostalias=\"mockhost3\" distribution-key=\"5\" />"+
+ " </group>"+
+ "</content>");
+ ContentSearchCluster s;
+
+ s = b.getSearch();
+ assertTrue(s.hasIndexedCluster());
+ assertNotNull(s.getIndexed());
+ assertEquals(6, b.getStorageNodes().getChildren().size());
+ assertTrue(b.getRootGroup().useCpuSocketAffinity());
+
+ assertThat(s.getSearchNodes().size(), is(6));
+ assertTrue(s.getSearchNodes().get(0).getAffinity().isPresent());
+ assertTrue(s.getSearchNodes().get(1).getAffinity().isPresent());
+ assertTrue(s.getSearchNodes().get(2).getAffinity().isPresent());
+ assertTrue(s.getSearchNodes().get(3).getAffinity().isPresent());
+ assertTrue(s.getSearchNodes().get(4).getAffinity().isPresent());
+ assertTrue(s.getSearchNodes().get(5).getAffinity().isPresent());
+ assertThat(s.getSearchNodes().get(0).getAffinity().get().cpuSocket(),is (0));
+ assertThat(s.getSearchNodes().get(1).getAffinity().get().cpuSocket(),is (1));
+ assertThat(s.getSearchNodes().get(2).getAffinity().get().cpuSocket(),is (2));
+ assertThat(s.getSearchNodes().get(3).getAffinity().get().cpuSocket(),is (0));
+ assertThat(s.getSearchNodes().get(4).getAffinity().get().cpuSocket(),is (1));
+ assertThat(s.getSearchNodes().get(5).getAffinity().get().cpuSocket(),is (0));
+
+ // TODO: Only needed for the search nodes anyway?
+ assertFalse(b.getStorageNodes().getChildren().get("0").getAffinity().isPresent());
+ assertFalse(b.getStorageNodes().getChildren().get("1").getAffinity().isPresent());
+ assertFalse(b.getStorageNodes().getChildren().get("2").getAffinity().isPresent());
+ assertFalse(b.getStorageNodes().getChildren().get("3").getAffinity().isPresent());
+ assertFalse(b.getStorageNodes().getChildren().get("4").getAffinity().isPresent());
+ assertFalse(b.getStorageNodes().getChildren().get("5").getAffinity().isPresent());
+ //assertThat(b.getStorageNodes().getChildren().get("0").getAffinity().get().cpuSocket(), is(0));
+ //assertThat(b.getStorageNodes().getChildren().get("1").getAffinity().get().cpuSocket(), is(1));
+ //assertThat(b.getStorageNodes().getChildren().get("2").getAffinity().get().cpuSocket(), is(2));
+ //assertThat(b.getStorageNodes().getChildren().get("3").getAffinity().get().cpuSocket(), is(0));
+ //assertThat(b.getStorageNodes().getChildren().get("4").getAffinity().get().cpuSocket(), is(1));
+ //assertThat(b.getStorageNodes().getChildren().get("5").getAffinity().get().cpuSocket(), is(0));
+
+ }
+
+ @Test
+ public void requireBug5357273() throws Exception {
+ try {
+ createContent(
+ " <content version='1.0' id='storage'>\n" +
+ " <redundancy>3</redundancy>\n" +
+ " <documents>"+
+ " <document type='music' mode='index'/>"+
+ " </documents>" +
+ " <group>\n" +
+ " <node hostalias='mockhost' distribution-key='0' />\n" +
+ " </group>\n" +
+ " <engine>\n" +
+ " <vds/>\n" +
+ " </engine>\n" +
+ " </content>\n");
+
+ assertFalse(true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ assertEquals("Persistence engine does not allow for indexed search. Please use <proton> as your engine.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void handleProtonTuning() throws Exception{
+ ContentCluster a = createContent(
+ "<content version =\"1.0\" id=\"a\">" +
+ " <redundancy>3</redundancy>" +
+ " <engine>" +
+ " <proton>" +
+ " <tuning>" +
+ " <searchnode>" +
+ " <summary>" +
+ " <store>" +
+ " <cache>" +
+ " <maxsize>8192</maxsize>" +
+ " <maxentries>32</maxentries>" +
+ " <compression>" +
+ " <type>lz4</type>" +
+ " <level>8</level>" +
+ " </compression>" +
+ " </cache>" +
+ " </store>" +
+ " <io>" +
+ " <read>directio</read>" +
+ " </io>" +
+ " </summary>" +
+ " </searchnode>" +
+ " </tuning>" +
+ " </proton>" +
+ " </engine>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <group>" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>" +
+ " </group>" +
+ "</content>"
+ );
+
+ assertTrue(a.getPersistence() instanceof ProtonEngine.Factory);
+ ProtonConfig.Builder pb = new ProtonConfig.Builder();
+ a.getSearch().getConfig(pb);
+ List<String> serialize = ConfigInstance.serialize(new ProtonConfig(pb));
+ String cfg = StringUtilities.implode(serialize.toArray(new String[serialize.size()]), "\n");
+ assertThat(cfg, containsString("summary.cache.maxbytes 8192"));
+ assertThat(cfg, containsString("summary.cache.initialentries 32"));
+ assertThat(cfg, containsString("summary.cache.compression.level 8"));
+ assertThat(cfg, containsString("summary.cache.compression.type LZ4"));
+ assertThat(cfg, containsString("summary.read.io DIRECTIO"));
+ }
+
+ @Test
+ public void requireThatUserConfigCanBeSpecifiedForASearchDefinition() throws Exception {
+ String services = getConfigOverrideServices(
+ "<node hostalias='mockhost' distribution-key='0'/>",
+ " <config name='mynamespace.myconfig'>" +
+ " <myfield>myvalue</myfield>" +
+ " </config>"
+ );
+
+ VespaModel m = new VespaModelCreatorWithMockPkg(createAppWithMusic(getHosts(), services)).create();
+ String configId = "clu/search/cluster.clu/music";
+ {
+ GenericConfig.GenericConfigBuilder builder =
+ new GenericConfig.GenericConfigBuilder(new ConfigDefinitionKey("myconfig", "mynamespace"), new ConfigPayloadBuilder());
+ m.getConfig(builder, configId);
+ assertEquals(builder.getPayload().getSlime().get().field("myfield").asString(), "myvalue");
+ }
+ }
+
+ @Test
+ public void requireOneTldPerSearchContainer() throws Exception {
+ ContentCluster content = createContent(
+ " <content version='1.0' id='storage'>\n" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <group>\n" +
+ " <node hostalias='mockhost' distribution-key='0' />\n" +
+ " </group>\n" +
+ " </content>\n" +
+ " <jdisc version='1.0' id='qrc'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='mockhost' />" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <jdisc version='1.0' id='qrc2'>" +
+ " <http>" +
+ " <server id ='server1' port='5000' />" +
+ " </http>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='mockhost' />" +
+ " <node hostalias='mockhost2' />" +
+ " </nodes>" +
+ " </jdisc>"
+
+ );
+ List<Dispatch> tlds = content.getSearch().getIndexed().getTLDs();
+
+ assertThat(tlds.get(0).getHostname(), is("node0"));
+ assertThat(tlds.get(1).getHostname(), is("node0"));
+ assertThat(tlds.get(2).getHostname(), is("node1"));
+
+ assertThat(tlds.size(), is(3));
+ }
+
+ @Test
+ @Ignore
+ public void ensureOverrideAppendedOnlyOnce() throws Exception {
+ ContentCluster content = createContent(
+ "<content version='1.0' id='search'>" +
+ " <config name=\"vespa.config.search.core.proton\">" +
+ " <numthreadspersearch>1</numthreadspersearch>" +
+ " <search>" +
+ " <mmap>" +
+ " <options><item>POPULATE</item></options>" +
+ " </mmap>" +
+ " </search>" +
+ " </config>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <group>" +
+ " <node hostalias='mockhost' distribution-key='0'/>" +
+ " </group>" +
+ "</content>");
+ ProtonConfig.Builder builder = new ProtonConfig.Builder();
+ content.getSearch().getIndexed().getSearchNode(0).cascadeConfig(builder);
+ content.getSearch().getIndexed().getSearchNode(0).addUserConfig(builder);
+ ProtonConfig config = new ProtonConfig(builder);
+ assertThat(config.search().mmap().options().size(), is(1));
+ assertThat(config.search().mmap().options(0), is(ProtonConfig.Search.Mmap.Options.POPULATE));
+ }
+
+ @Test
+ public void ensurePruneRemovedDocumentsAgeForHostedVespa() throws Exception {
+ {
+ ContentCluster contentNonHosted = createContent("<content version='1.0' id='search'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <nodes>" +
+ " <node hostalias='mockhost' distribution-key='0'/>" +
+ " </nodes>" +
+ "</content>");
+ ProtonConfig configNonHosted = getProtonConfig(contentNonHosted);
+ ProtonConfig defaultConfig = new ProtonConfig(new ProtonConfig.Builder());
+ assertEquals(defaultConfig.pruneremoveddocumentsage(), configNonHosted.pruneremoveddocumentsage(), 0.001);
+ }
+
+ {
+ String hostedXml = "<services>" +
+ "<content version='1.0' id='search'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <nodes count='1'/>" +
+ "</content>" +
+ "</services>";
+
+ DeployState.Builder deployStateBuilder = new DeployState.Builder().properties(
+ new DeployProperties.Builder()
+ .hostedVespa(true)
+ .build());
+ VespaModel model = new VespaModelCreatorWithMockPkg(new MockApplicationPackage.Builder()
+ .withServices(hostedXml)
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build())
+ .create(deployStateBuilder);
+ ProtonConfig config = getProtonConfig(model.getContentClusters().values().iterator().next());
+ assertEquals(349260.0, config.pruneremoveddocumentsage(), 0.001);
+ }
+ }
+
+ private ProtonConfig getProtonConfig(ContentCluster content) {
+ ProtonConfig.Builder configBuilder = new ProtonConfig.Builder();
+ content.getSearch().getIndexed().getSearchNode(0).cascadeConfig(configBuilder);
+ content.getSearch().getIndexed().getSearchNode(0).addUserConfig(configBuilder);
+
+ return new ProtonConfig(configBuilder);
+ }
+
+ ApplicationPackage createAppWithMusic(String hosts, String services) {
+ return new MockApplicationPackage.Builder()
+ .withHosts(hosts)
+ .withServices(services)
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilderTest.java
new file mode 100644
index 00000000000..6a9450b3d4e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/DomSearchTuningBuilderTest.java
@@ -0,0 +1,228 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.collections.CollectionUtil;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.model.search.Tuning;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class DomSearchTuningBuilderTest extends DomBuilderTest {
+
+ private static final double DELTA = 0.000001;
+
+ private static Element parseXml(String... xmlLines) {
+ return parse("<tuning>",
+ "<searchnode>",
+ CollectionUtil.mkString(Arrays.asList(xmlLines), "\n"),
+ "</searchnode>",
+ "</tuning>");
+ }
+
+ private Tuning newTuning(String xml) {
+ return createTuning(parse(xml));
+ }
+
+ private Tuning createTuning(Element xml) {
+ DomSearchTuningBuilder b = new DomSearchTuningBuilder();
+ return b.build(root, xml);
+ }
+
+ String getProtonCfg(Tuning tuning) {
+ ProtonConfig.Builder pb = new ProtonConfig.Builder();
+ tuning.getConfig(pb);
+ return StringUtilities.implode(ConfigInstance.serialize(new ProtonConfig(pb)).toArray(new String[0]), "\n");
+ }
+
+ @Test
+ public void requireThatNullDispatchIsSafe() {
+ Tuning tuning = newTuning("<tuning />");
+ assertNull(tuning.dispatch);
+ }
+
+ @Test
+ public void requireThatEmptyDispatchIsSafe() {
+ Tuning tuning = newTuning("<tuning><dispatch/></tuning>");
+ Tuning.Dispatch dispatch = tuning.dispatch;
+ assertNotNull(dispatch);
+ assertNull(dispatch.maxHitsPerPartition);
+ }
+
+ @Test
+ public void requireThatDispatchSettingsAreParsed() {
+ Tuning tuning = createTuning(parse("<tuning>" +
+ " <dispatch>" +
+ " <max-hits-per-partition>69</max-hits-per-partition>" +
+ " </dispatch>" +
+ "</tuning>"));
+ Tuning.Dispatch dispatch = tuning.dispatch;
+ assertNotNull(dispatch);
+ assertNotNull(dispatch.maxHitsPerPartition);
+ assertEquals(69, dispatch.maxHitsPerPartition.intValue());
+ }
+
+ @Test
+ public void requireThatWeCanParseRequestThreadsTag() {
+ Tuning t = createTuning(parseXml("<requestthreads>",
+ "<search>123</search>",
+ "<persearch>34</persearch>",
+ "<summary>456</summary>",
+ "</requestthreads>"));
+ assertEquals(123, t.searchNode.threads.numSearchThreads.longValue());
+ assertEquals(456, t.searchNode.threads.numSummaryThreads.longValue());
+ String cfg = getProtonCfg(t);
+ assertThat(cfg, containsString("numsearcherthreads 123"));
+ assertThat(cfg, containsString("numthreadspersearch 34"));
+ assertThat(cfg, containsString("numsummarythreads 456"));
+ }
+
+ @Test
+ public void requireThatWeCanParseFlushStrategyTag() {
+ Tuning t = createTuning(parseXml("<flushstrategy>","<native>",
+ "<total>",
+ "<maxmemorygain>900</maxmemorygain>",
+ "<diskbloatfactor>8.7</diskbloatfactor>",
+ "</total>",
+ "<component>",
+ "<maxmemorygain>600</maxmemorygain>",
+ "<diskbloatfactor>5.4</diskbloatfactor>",
+ "<maxage>300</maxage>",
+ "</component>",
+ "<transactionlog>",
+ "<maxentries>200</maxentries>",
+ "<maxsize>1024</maxsize>",
+ "</transactionlog>",
+ "</native>","</flushstrategy>"));
+ assertEquals(900, t.searchNode.strategy.totalMaxMemoryGain.longValue());
+ assertEquals(8.7, t.searchNode.strategy.totalDiskBloatFactor.doubleValue(), DELTA);
+ assertEquals(600, t.searchNode.strategy.componentMaxMemoryGain.longValue());
+ assertEquals(5.4, t.searchNode.strategy.componentDiskBloatFactor.doubleValue(), DELTA);
+ assertEquals(300, t.searchNode.strategy.componentMaxage.doubleValue(), DELTA);
+ assertEquals(200, t.searchNode.strategy.transactionLogMaxEntries.longValue());
+ assertEquals(1024, t.searchNode.strategy.transactionLogMaxSize.longValue());
+ String cfg = getProtonCfg(t);
+ assertThat(cfg, containsString("flush.memory.maxmemory 900"));
+ assertThat(cfg, containsString("flush.memory.diskbloatfactor 8.7"));
+ assertThat(cfg, containsString("flush.memory.each.maxmemory 600"));
+ assertThat(cfg, containsString("flush.memory.each.diskbloatfactor 5.4"));
+ assertThat(cfg, containsString("flush.memory.maxage.time 300"));
+ assertThat(cfg, containsString("flush.memory.maxage.serial 200"));
+ assertThat(cfg, containsString("flush.memory.maxtlssize 1024"));
+ }
+
+ @Test
+ public void requireThatWeCanParseResizingTag() {
+ Tuning t = createTuning(parseXml("<resizing>",
+ "<initialdocumentcount>128</initialdocumentcount>",
+ "</resizing>"));
+ assertEquals(128, t.searchNode.resizing.initialDocumentCount.intValue());
+ String cfg = getProtonCfg(t);
+ assertThat(cfg, containsString("grow.initial 128"));
+ }
+
+ @Test
+ public void requireThatWeCanParseIndexTag() {
+ Tuning t = createTuning(parseXml("<index>", "<io>",
+ "<write>directio</write>",
+ "<read>normal</read>",
+ "<search>mmap</search>",
+ "</io>", "</index>"));
+ assertEquals(Tuning.SearchNode.IoType.DIRECTIO, t.searchNode.index.io.write);
+ assertEquals(Tuning.SearchNode.IoType.NORMAL, t.searchNode.index.io.read);
+ assertEquals(Tuning.SearchNode.IoType.MMAP, t.searchNode.index.io.search);
+ String cfg = getProtonCfg(t);
+ assertThat(cfg, containsString("indexing.write.io DIRECTIO"));
+ assertThat(cfg, containsString("indexing.read.io NORMAL"));
+ assertThat(cfg, containsString("search.io MMAP"));
+ }
+
+ @Test
+ public void requireThatWeCanParseAttributeTag() {
+ Tuning t = createTuning(parseXml("<attribute>", "<io>",
+ "<write>directio</write>",
+ "</io>", "</attribute>"));
+ assertEquals(Tuning.SearchNode.IoType.DIRECTIO, t.searchNode.attribute.io.write);
+ String cfg = getProtonCfg(t);
+ assertThat(cfg, containsString("attribute.write.io DIRECTIO"));
+ }
+
+ @Test
+ public void requireThatWeCanParseSummaryTag() {
+ Tuning t = createTuning(parseXml("<summary>",
+ "<io>",
+ "<write>directio</write>",
+ "<read>directio</read>",
+ "</io>",
+ "<store>",
+ "<cache>",
+ "<maxsize>128</maxsize>",
+ "<maxentries>64</maxentries>",
+ "<compression>",
+ "<type>none</type>",
+ "<level>3</level>",
+ "</compression>",
+ "</cache>",
+ "<logstore>",
+ "<maxfilesize>512</maxfilesize>",
+ "<maxdiskbloatfactor>1.4</maxdiskbloatfactor>",
+ "<minfilesizefactor>0.3</minfilesizefactor>",
+ "<numthreads>7</numthreads>",
+ "<chunk>",
+ "<maxsize>256</maxsize>",
+ "<maxentries>32</maxentries>",
+ "<compression>",
+ "<type>lz4</type>",
+ "<level>5</level>",
+ "</compression>",
+ "</chunk>",
+ "</logstore>",
+ "</store>",
+ "</summary>"));
+ assertEquals(Tuning.SearchNode.IoType.DIRECTIO, t.searchNode.summary.io.write);
+ assertEquals(Tuning.SearchNode.IoType.DIRECTIO, t.searchNode.summary.io.read);
+ assertEquals(128, t.searchNode.summary.store.cache.maxSize.longValue());
+ assertEquals(64, t.searchNode.summary.store.cache.maxEntries.longValue());
+ assertEquals(Tuning.SearchNode.Summary.Store.Compression.Type.NONE,
+ t.searchNode.summary.store.cache.compression.type);
+ assertEquals(3, t.searchNode.summary.store.cache.compression.level.intValue());
+ assertEquals(512, t.searchNode.summary.store.logStore.maxFileSize.longValue());
+ assertEquals(1.4, t.searchNode.summary.store.logStore.maxDiskBloatFactor, DELTA);
+ assertEquals(0.3, t.searchNode.summary.store.logStore.minFileSizeFactor, DELTA);
+ assertEquals(7, t.searchNode.summary.store.logStore.numThreads.intValue());
+ assertEquals(256, t.searchNode.summary.store.logStore.chunk.maxSize.intValue());
+ assertEquals(32, t.searchNode.summary.store.logStore.chunk.maxEntries.intValue());
+ assertEquals(Tuning.SearchNode.Summary.Store.Compression.Type.LZ4,
+ t.searchNode.summary.store.logStore.chunk.compression.type);
+ assertEquals(5, t.searchNode.summary.store.logStore.chunk.compression.level.intValue());
+ String cfg = getProtonCfg(t);
+ assertThat(cfg, containsString("summary.write.io DIRECTIO"));
+ assertThat(cfg, containsString("summary.read.io DIRECTIO"));
+ assertThat(cfg, containsString("summary.cache.maxbytes 128"));
+ assertThat(cfg, containsString("summary.cache.initialentries 64"));
+ assertThat(cfg, containsString("summary.cache.compression.type NONE"));
+ assertThat(cfg, containsString("summary.cache.compression.level 3"));
+ assertThat(cfg, containsString("summary.log.maxfilesize 512"));
+ assertThat(cfg, containsString("summary.log.maxdiskbloatfactor 1.4"));
+ assertThat(cfg, containsString("summary.log.minfilesizefactor 0.3"));
+ assertThat(cfg, containsString("summary.log.chunk.maxbytes 256"));
+ assertThat(cfg, containsString("summary.log.chunk.maxentries 32"));
+ assertThat(cfg, containsString("summary.log.chunk.compression.type LZ4"));
+ assertThat(cfg, containsString("summary.log.chunk.compression.level 5"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilderTest.java
new file mode 100644
index 00000000000..8239cb9cde0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/LegacyConfigModelBuilderTest.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.text.XML;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class LegacyConfigModelBuilderTest {
+ @Test
+ public void testThatProducerIsInserted() {
+ String services = "<foo><config name=\"bar\"><key>value</key></config></foo>";
+ ModelBuilder builder = new ModelBuilder();
+ Model model = builder.build(DeployState.createTestState(new MockApplicationPackage.Builder().withServices(services).build()),
+ null, new MockRoot(), XML.getDocument(services).getDocumentElement());
+ assertThat(model.getContext().getParentProducer().getUserConfigs().size(), is(1));
+ }
+
+ public static class Model extends ConfigModel {
+
+ private final ConfigModelContext context;
+
+ /**
+ * Constructs a new config model given a context.
+ *
+ * @param modelContext The model context.
+ */
+ public Model(ConfigModelContext modelContext) {
+ super(modelContext);
+ this.context = modelContext;
+ }
+
+ public ConfigModelContext getContext() {
+ return context;
+ }
+ }
+ private static class ModelBuilder extends LegacyConfigModelBuilder<Model> {
+
+ public ModelBuilder() {
+ super(Model.class);
+ }
+
+ @Override
+ public void doBuild(Model model, Element element, ConfigModelContext modelContext) {
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return Arrays.asList(ConfigModelId.fromName("foo"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilderTest.java
new file mode 100755
index 00000000000..38925eade28
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/VespaDomBuilderTest.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom;
+
+import com.yahoo.config.application.Xml;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigPayloadBuilder;
+import com.yahoo.vespa.config.GenericConfig;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.HostSystem;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ */
+public class VespaDomBuilderTest {
+
+ final static String hosts = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<hosts>" +
+ " <host name=\"localhost\">" +
+ " <alias>node1</alias>" +
+ " <alias>node2</alias>" +
+ " </host>" +
+ "</hosts>";
+
+ final static String services = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services>" +
+ " <config name=\"standard\">" +
+ " <basicStruct>" +
+ " <stringVal>default</stringVal>" +
+ " </basicStruct>" +
+ " </config> " +
+ " <config name=\"container.core.container-http\"><port><search>6745</search></port></config>" +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node1\" />" +
+ " </admin>" +
+ " <container version=\"1.0\">" +
+ " <config name=\"standard\">" +
+ " <basicStruct>" +
+ " <stringVal>qrservers</stringVal>" +
+ " </basicStruct>" +
+ " </config> " +
+ " <nodes>\n" +
+ " <node hostalias=\"node1\"/>\n" +
+ " </nodes>\n" +
+ " </container>\n" +
+ "</services>";
+
+ final static String servicesWithNamespace = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services>" +
+ " <config name=\"testnamespace\" namespace=\"foo\">" +
+ " <basicStruct>" +
+ " <stringVal>default</stringVal>" +
+ " </basicStruct>" +
+ " </config> " +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node1\" />" +
+ " </admin>" +
+ "</services>";
+
+ final static String servicesWithNamespace2 = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services>" +
+ " <config name=\"foo.testnamespace\">" +
+ " <basicStruct>" +
+ " <stringVal>default</stringVal>" +
+ " </basicStruct>" +
+ " </config> " +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node1\" />" +
+ " </admin>" +
+ "</services>";
+
+
+ @Test
+ public void testUserConfigsWithNamespace() throws Exception {
+ VespaModel model = createModel(hosts, servicesWithNamespace);
+
+ GenericConfig.GenericConfigBuilder builder =
+ new GenericConfig.GenericConfigBuilder(new ConfigDefinitionKey("testnamespace", "foo"), new ConfigPayloadBuilder());
+ model.getConfig(builder, "admin");
+ assertEquals(builder.getPayload().toString(), "{\n" +
+ " \"basicStruct\": {\n" +
+ " \"stringVal\": \"default\"\n" +
+ " }\n" +
+ "}\n");
+
+ model = createModel(hosts, servicesWithNamespace2);
+
+ builder = new GenericConfig.GenericConfigBuilder(new ConfigDefinitionKey("testnamespace", "foo"), new ConfigPayloadBuilder());
+ model.getConfig(builder, "admin");
+ assertEquals(builder.getPayload().toString(), "{\n" +
+ " \"basicStruct\": {\n" +
+ " \"stringVal\": \"default\"\n" +
+ " }\n" +
+ "}\n");
+ }
+
+ @Test
+ public void testGetElement() {
+ Element e = Xml.getElement(new StringReader("<searchchain><foo>sdf</foo></searchchain>"));
+ assertEquals(e.getTagName(), "searchchain");
+ assertEquals(XML.getChild(e, "foo").getTagName(), "foo");
+ assertEquals(XML.getValue(XML.getChild(e, "foo")), "sdf");
+ }
+
+ @Test
+ public void testHostSystem() throws IOException, SAXException {
+ VespaModel model = createModel(hosts, services);
+ HostSystem hostSystem = model.getHostSystem();
+ System.out.println(hostSystem);
+ assertThat(hostSystem.getHosts().size(), is(1));
+ HostResource host = hostSystem.getHosts().get(0);
+ assertThat(host, is(hostSystem.getHostByHostname(host.getHostName())));
+ assertNotNull(hostSystem.getHost("node1"));
+ assertThat(hostSystem.toString(), is("host '" + host.getHostName() + "'"));
+ }
+
+ private VespaModel createModel(String hosts, String services) {
+ VespaModelCreatorWithMockPkg creator = new VespaModelCreatorWithMockPkg(hosts, services);
+ return creator.create();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilderTest.java
new file mode 100644
index 00000000000..482e1070a9e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/DependenciesBuilderTest.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains;
+
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Basic tests of DependencyBuilder
+ * @author tonytv
+ */
+public class DependenciesBuilderTest extends DomBuilderTest {
+ private Set<String> set(String str) {
+ Set<String> symbols = new HashSet<>();
+ for (String symbol : str.split(",")) {
+ symbols.add(symbol);
+ }
+ return symbols;
+ }
+
+ @Test
+ public void testBuildDependencies() {
+ DependenciesBuilder dependenciesBuilder = new DependenciesBuilder(parse(
+ "<searcher provides='symbol1 symbol2 ' before='p1' after=' s1' >",
+ " <provides> symbol3 </provides>",
+ " <provides> symbol4 </provides>",
+ " <before> p2 </before>",
+ " <after>s2</after>",
+ "</searcher>"));
+
+ Dependencies dependencies = dependenciesBuilder.build();
+
+ assertEquals(dependencies.provides(),
+ set("symbol1,symbol2,symbol3,symbol4"));
+
+ assertEquals(dependencies.before(),
+ set("p1,p2"));
+
+ assertEquals(dependencies.after(),
+ set("s1,s2"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilderTest.java
new file mode 100644
index 00000000000..32f3453c2ee
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomFederationSearcherBuilderTest.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.search.federation.FederationConfig;
+import com.yahoo.search.searchchain.model.federation.FederationSearcherModel;
+import com.yahoo.vespa.model.container.search.searchchain.FederationSearcher;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Test of DomFederationSearcherBuilder.
+ * @author tonytv
+ */
+public class DomFederationSearcherBuilderTest extends DomBuilderTest {
+
+ @Test
+ public void ensureCorrectModel() {
+ FederationSearcher searcher = new DomFederationSearcherBuilder().doBuild(root, parse(
+ "<federation id='theId'>",
+ " <provides>p2</provides>",
+ " <source-set inherits=\"default\" />",
+ " <source id='source1'>",
+ " <federationoptions optional='true' />",
+ " </source>",
+ " <source id='source2' />",
+ "</federation>"));
+
+ FederationSearcherModel model = searcher.model;
+
+ assertEquals("theId", model.bundleInstantiationSpec.id.stringValue());
+ assertEquals(com.yahoo.search.federation.FederationSearcher.class.getName(),
+ model.bundleInstantiationSpec.classId.stringValue());
+
+ assertEquals(2, model.targets.size());
+ assertTrue("source-set option was ignored", model.inheritDefaultSources);
+
+ assertThat(targetNames(model.targets),
+ hasItems("source1", "source2"));
+
+ }
+
+ private List<String> targetNames(List<FederationSearcherModel.TargetSpec> targets) {
+ List<String> res = new ArrayList<>();
+ for (FederationSearcherModel.TargetSpec target : targets) {
+ res.add(target.sourceSpec.getName());
+ }
+ return res;
+ }
+
+ @Test
+ public void require_that_target_selector_can_be_configured() {
+ FederationSearcher searcher = new DomFederationSearcherBuilder().doBuild(root, parse(
+ "<federation id='federation-id'>",
+ " <target-selector id='my-id' class='my-class' />",
+ "</federation>"));
+
+ String targetSelectorId = "my-id@federation-id";
+
+ AbstractConfigProducer<?> targetSelector = searcher.getChildren().get(targetSelectorId);
+ assertNotNull("No target selector child found", targetSelector);
+
+ FederationConfig.Builder builder = new FederationConfig.Builder();
+ searcher.getConfig(builder);
+ assertThat(new FederationConfig(builder).targetSelector(), is(targetSelectorId));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilderTest.java
new file mode 100755
index 00000000000..ef3e84a300a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomProviderBuilderTest.java
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.search.federation.ProviderConfig;
+import com.yahoo.vespa.model.builder.xml.dom.chains.ComponentsBuilder;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import com.yahoo.vespa.model.container.search.searchchain.HttpProvider;
+import com.yahoo.vespa.model.container.search.searchchain.HttpProviderSearcher;
+import com.yahoo.vespa.model.container.search.searchchain.Provider;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author gjoranv
+ */
+public class DomProviderBuilderTest extends DomBuilderTest {
+
+ private static final Element noProxy = parse(
+ "<provider id='yca-provider' type='vespa' yca-application-id='my-app'>",
+ " <nodes>",
+ " <node host='sourcehost' port='12'/>",
+ " </nodes>",
+ "</provider>");
+
+ private static final Element defaultProxy = parse(
+ "<provider id='yca-provider' type='vespa' yca-application-id='my-app'>",
+ " <yca-proxy/>",
+ " <nodes>",
+ " <node host='sourcehost' port='12'/>",
+ " </nodes>",
+ "</provider>");
+
+ private static final Element proprietaryProxy = parse(
+ "<provider id='yca-provider' type='vespa' yca-application-id='my-app'>",
+ " <yca-proxy host='my-host' port='80'/>",
+ " <nodes>",
+ " <node host='sourcehost' port='12'/>",
+ " </nodes>",
+ "</provider>");
+
+ private static final Element illegal_proxyWithoutId= parse(
+ "<provider id='yca-provider' type='vespa'>",
+ " <yca-proxy host='my-host' port='80'/>",
+ " <nodes>",
+ " <node host='sourcehost' port='12'/>",
+ " </nodes>",
+ "</provider>");
+
+ private Provider provider;
+
+ @Test
+ public void testYcaConfig_noProxy() {
+ provider = new DomProviderBuilder(new HashMap<String, ComponentsBuilder.ComponentType>()).doBuild(root, noProxy);
+
+ ChainedComponent providerSearcher = provider.getInnerComponents().iterator().next();
+ assertThat(providerSearcher, instanceOf(HttpProviderSearcher.class));
+
+ ProviderConfig.Builder providerBuilder = new ProviderConfig.Builder();
+ ((HttpProvider)provider).getConfig(providerBuilder);
+ ProviderConfig providerConfig = new ProviderConfig(providerBuilder);
+ assertThat(providerConfig.yca().applicationId(), is("my-app"));
+ assertThat(providerConfig.yca().useProxy(), is(false));
+ }
+
+ @Test
+ public void testYcaConfig_defaultProxy() {
+ provider = new DomProviderBuilder(new HashMap<String, ComponentsBuilder.ComponentType>()).doBuild(root, defaultProxy);
+
+ ProviderConfig.Builder providerBuilder = new ProviderConfig.Builder();
+ ((HttpProvider)provider).getConfig(providerBuilder);
+ ProviderConfig providerConfig = new ProviderConfig(providerBuilder);
+
+ assertThat(providerConfig.yca().applicationId(), is("my-app"));
+ assertThat(providerConfig.yca().useProxy(), is(true));
+ assertThat(providerConfig.yca().host(), is("yca-proxy.corp.yahoo.com")); // default from def-file
+ assertThat(providerConfig.yca().port(), is(3128)); // default from def-file
+ }
+
+ @Test
+ public void testYcaConfig_proprietaryProxy() {
+ provider = new DomProviderBuilder(new HashMap<String, ComponentsBuilder.ComponentType>()).doBuild(root, proprietaryProxy);
+
+ ProviderConfig.Builder providerBuilder = new ProviderConfig.Builder();
+ ((HttpProvider)provider).getConfig(providerBuilder);
+ ProviderConfig providerConfig = new ProviderConfig(providerBuilder);
+
+ assertThat(providerConfig.yca().applicationId(), is("my-app"));
+ assertThat(providerConfig.yca().useProxy(), is(true));
+ assertThat(providerConfig.yca().host(), is("my-host"));
+ assertThat(providerConfig.yca().port(), is(80));
+ }
+
+ @Test
+ public void testFail_ycaProxyWithoutId() {
+ try {
+ provider = new DomProviderBuilder(new HashMap<String, ComponentsBuilder.ComponentType>()).doBuild(root, illegal_proxyWithoutId);
+ fail("Expected exception upon illegal xml.");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("Provider 'yca-provider' must have a YCA application ID, since a YCA proxy is given"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilderTest.java
new file mode 100644
index 00000000000..98469bf26c1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearchChainsBuilderTest.java
@@ -0,0 +1,204 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.container.core.ChainsConfig;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import com.yahoo.vespa.model.container.search.searchchain.*;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static com.yahoo.container.core.ChainsConfig.Chains;
+import static com.yahoo.container.core.ChainsConfig.Components;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.hamcrest.Matchers.containsString;
+
+/**
+ * Test of Search chains builder.
+ * @author tonytv
+ */
+public class DomSearchChainsBuilderTest extends DomBuilderTest {
+ private SearchChains searchChains;
+
+ private static final Element element = parse(
+ "<searchchains>",
+ " <searcher id='searcher:1'/>",
+
+ " <provider id='provider:1' type='vespa' inherits='parentChain1 parentChain2' excludes='ExcludedSearcher1 ExcludedSearcher2'",
+ " cacheweight='2.3'>",
+ " <federationoptions optional='true' timeout='2.3 s' />",
+ " <nodes>",
+ " <node host='sourcehost' port='12'/>",
+ " </nodes>",
+
+ " <source id='source:1' inherits='parentChain3 parentChain4' excludes='ExcludedSearcher3 ExcludedSearcher4'>",
+ " <federationoptions timeout='12 ms' />",
+ " </source>",
+
+ " </provider>",
+
+ " <searchchain id='default'>",
+ " <federation id='federationSearcher'>",
+ " <source id='mysource'>",
+ " <federationoptions optional='false' />",
+ " </source>",
+ " </federation>",
+ " </searchchain>",
+
+ "</searchchains>");
+
+
+ @Before
+ public void createSearchChains() {
+ searchChains = new DomSearchChainsBuilder().build(root, element);
+ }
+
+ @Test
+ public void referToFederationAsSearcher() {
+ final Element element = parse(
+ "<searchchains>",
+ " <federation id='federationSearcher'>",
+ " <source id='mysource'>",
+ " <federationoptions optional='false' />",
+ " </source>",
+ " </federation>",
+
+ " <searchchain id='default'>",
+ " <searcher id='federationSearcher'/>",
+ " </searchchain>",
+ "</searchchains>");
+
+ try {
+ new DomSearchChainsBuilder().build(new MockRoot(), element);
+ fail("Expected exception when referring to an outer 'federation' as a 'searcher'.");
+ } catch (RuntimeException e) {
+ assertThat(e.getMessage(), containsString("Two different types declared for the component with name 'federationSearcher'"));
+ }
+ }
+
+ @Test
+ public void ensureSearchChainsExists() {
+ for (String id : Arrays.asList("provider:1", "source:1@provider:1", "default")) {
+ assertNotNull("Missing search chain " + id, getSearchChain(id));
+ }
+ }
+
+ @Test
+ public void ensureSearcherExists() {
+ assertThat(searchChains.allComponents(), hasItem(searcherWithId("searcher:1")));
+ }
+
+ private Matcher<ChainedComponent<?>> searcherWithId(final String componentId) {
+ return new BaseMatcher<ChainedComponent<?>>() {
+ @Override
+ public boolean matches(Object o) {
+ return o instanceof ChainedComponent &&
+ ((ChainedComponent) o).getComponentId().equals(new ComponentId(componentId));
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("a searcher with id ").appendValue(componentId);
+ }
+ };
+ }
+
+ @Test
+ public void checkProviderFederationOptions() {
+ FederationOptions options = getProvider().federationOptions();
+
+ assertEquals(true, options.getOptional());
+ assertEquals(2300, options.getTimeoutInMilliseconds());
+ }
+
+ @Test
+ public void checkSourceFederationOptions() {
+ FederationOptions options = getSource().federationOptions();
+
+ assertEquals(true, options.getOptional()); //inherited
+ assertEquals(12, options.getTimeoutInMilliseconds());
+ }
+
+ @Test
+ public void checkDefaultTargets() {
+ Collection<? extends GenericTarget> defaultTargets =
+ getProvider().defaultFederationTargets();
+
+ assertEquals(1, defaultTargets.size());
+ assertEquals(getSearchChain("source:1@provider:1"), first(defaultTargets));
+ }
+
+ @Test
+ public void checkInnerSearcherIdIsNestedInSearchChainId() {
+ ChainsConfig.Builder builder = new ChainsConfig.Builder();
+ searchChains.getConfig(builder);
+ ChainsConfig config = new ChainsConfig(builder);
+
+ checkInnerSearcherIdIsNestedInSearchChainId(config, "federationSearcher", "default");
+ checkInnerSearcherIdIsNestedInSearchChainId(config, "VespaSearcher", "provider");
+ }
+
+ private void checkInnerSearcherIdIsNestedInSearchChainId(ChainsConfig config,
+ String partOfSearcherName,
+ String searchChainName) {
+ Components searcher = getSearcherConfig(config.components(), partOfSearcherName);
+ ComponentId searcherId = ComponentId.fromString(searcher.id());
+
+ assertThat(searcherId.getNamespace(), is(getSearchChain(searchChainName).getComponentId()));
+
+ Chains searchChain = getSearchChainConfig(config.chains(), searchChainName);
+ assertThat(ComponentId.fromString(searchChain.components(0)), is(searcherId));
+ }
+
+ private Chains getSearchChainConfig(List<Chains> searchChains,
+ String searchChainName) {
+ for (Chains searchChain : searchChains) {
+ if (ComponentId.fromString(searchChain.id()).getName().equals(searchChainName))
+ return searchChain;
+ }
+ fail("No search chain matching " + searchChainName);
+ return null;
+ }
+
+ private Components getSearcherConfig(List<Components> searchers, String partOfId) {
+ for (Components searcher : searchers) {
+ if (searcher.id().contains(partOfId))
+ return searcher;
+ }
+ fail("No searcher matching " + partOfId);
+ return null;
+ }
+
+ private static <T> T first(Iterable<T> coll) {
+ return coll.iterator().next();
+ }
+
+ private Provider getProvider() {
+ return (Provider)getSearchChain("provider:1");
+ }
+
+ private Source getSource() {
+ return first(getProvider().getSources());
+ }
+
+ private SearchChain getSearchChain(String componentSpecification) {
+ return searchChains.allChains().getComponent(new ComponentSpecification(componentSpecification));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilderTest.java
new file mode 100644
index 00000000000..b77161ef415
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/chains/search/DomSearcherBuilderTest.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.builder.xml.dom.chains.search;
+
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertEquals;
+
+
+/**
+ * @author tonytv
+ */
+public class DomSearcherBuilderTest extends DomBuilderTest {
+ @Test
+ public void ensureCorrectModel() {
+ ChainedComponent<ChainedComponentModel> searcher = new DomSearcherBuilder().doBuild(root, parse(
+ "<searcher id='theId' class='theclassid' bundle='thebundle' provides='p1'>",
+ " <provides>p2</provides>",
+ "</searcher>"));
+
+ ChainedComponentModel model = searcher.model;
+ assertEquals(2, model.dependencies.provides().size());
+
+ BundleInstantiationSpecification instantiationSpecification = model.bundleInstantiationSpec;
+ assertEquals("theId", instantiationSpecification.id.stringValue());
+ assertEquals("theclassid", instantiationSpecification.classId.stringValue());
+ assertEquals("thebundle", instantiationSpecification.bundle.stringValue());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/searchchains/.gitignore b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/searchchains/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/builder/xml/dom/searchchains/.gitignore
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/clients/test/Gateway20TestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/clients/test/Gateway20TestCase.java
new file mode 100644
index 00000000000..5987ab8410a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/clients/test/Gateway20TestCase.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients.test;
+
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.container.QrConfig;
+import com.yahoo.container.QrConfig.Builder;
+import com.yahoo.container.core.ContainerHttpConfig;
+import com.yahoo.net.HostName;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:gunnarga@yahoo-inc.com">Gunnar Gauslaa Bergem</a>
+ */
+public class Gateway20TestCase {
+ private static String hostname = HostName.getLocalhost(); // Using the same way of getting hostname as filedistribution model
+
+ @Test
+ public void testSimpleDocprocV3() throws Exception {
+ VespaModel model = new VespaModelCreatorWithFilePkg("src/test/cfg/clients/simpleconfig.v2.docprocv3").create();
+ QrConfig qrConfig = new QrConfig((Builder) model.getConfig(new QrConfig.Builder(), "container/container.0"));
+ assertEquals(qrConfig.rpc().enabled(), true);
+ assertEquals("filedistribution/" + hostname, qrConfig.filedistributor().configid());
+ assertEquals("container.container.0", qrConfig.discriminator());
+
+ ContainerHttpConfig cHConfig = new ContainerHttpConfig((ContainerHttpConfig.Builder) model.getConfig(new ContainerHttpConfig.Builder(), "container/container.0"));
+ assertTrue(cHConfig.enabled());
+ assertEquals(Defaults.getDefaults().vespaWebServicePort(), cHConfig.port().search());
+
+ ComponentsConfig componentsConfig = new ComponentsConfig((ComponentsConfig.Builder) model.getConfig(new ComponentsConfig.Builder(), "container/container.0"));
+ ArrayList<String> components = new ArrayList<>();
+ for (ComponentsConfig.Components component : componentsConfig.components()) {
+ components.add(component.id());
+ }
+ List<String> expectedComponents = Arrays.asList("com.yahoo.docproc.jdisc.DocumentProcessingHandler",
+ "com.yahoo.feedhandler.VespaFeedHandler",
+ "com.yahoo.feedhandler.VespaFeedHandlerCompatibility",
+ "com.yahoo.feedhandler.VespaFeedHandlerGet",
+ "com.yahoo.feedhandler.VespaFeedHandlerRemove",
+ "com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation",
+ "com.yahoo.feedhandler.VespaFeedHandlerStatus",
+ "com.yahoo.feedhandler.VespaFeedHandlerVisit",
+ "com.yahoo.search.handler.SearchHandler",
+ "com.yahoo.container.jdisc.state.StateHandler");
+ assertTrue(components.containsAll(expectedComponents));
+ }
+
+ @Test
+ public void testAdvanced() throws Exception {
+ VespaModel model = new VespaModelCreatorWithFilePkg("src/test/cfg/clients/advancedconfig.v2").create();
+
+ QrConfig qrConfig = new QrConfig((Builder) model.getConfig(new QrConfig.Builder(), "container/container.0"));
+ assertEquals(qrConfig.rpc().enabled(), true);
+ assertEquals(qrConfig.filedistributor().configid(), "filedistribution/" + hostname);
+ assertEquals("container.container.0", qrConfig.discriminator());
+
+ ContainerHttpConfig cHConfig = new ContainerHttpConfig((ContainerHttpConfig.Builder) model.getConfig(new ContainerHttpConfig.Builder(), "container/container.0"));
+ assertTrue(cHConfig.enabled());
+ assertEquals(Defaults.getDefaults().vespaWebServicePort(), cHConfig.port().search());
+
+ qrConfig = new QrConfig((Builder) model.getConfig(new QrConfig.Builder(), "container/container.0"));
+ assertEquals(qrConfig.rpc().enabled(), true);
+ assertEquals(qrConfig.filedistributor().configid(), "filedistribution/" + hostname);
+
+ cHConfig = new ContainerHttpConfig((ContainerHttpConfig.Builder) model.getConfig(new ContainerHttpConfig.Builder(), "container/container.0"));
+ assertEquals(cHConfig.enabled(), true);
+ assertEquals(Defaults.getDefaults().vespaWebServicePort(), cHConfig.port().search());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/clients/test/SpoolerTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/clients/test/SpoolerTestCase.java
new file mode 100644
index 00000000000..9a6c57da141
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/clients/test/SpoolerTestCase.java
@@ -0,0 +1,139 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.clients.test;
+
+import com.yahoo.vespa.config.content.spooler.SpoolerConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.CommonVespaModelSetup;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+import java.util.*;
+
+/**
+ * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a>
+ */
+public class SpoolerTestCase extends junit.framework.TestCase {
+
+ public void testSimple() throws Exception {
+ VespaModel model = createModel("src/test/cfg/clients/simpleconfig.v2.docprocv3");
+
+ SpoolerConfig.Builder builder = new SpoolerConfig.Builder();
+ SpoolerConfig.Parsers.Builder parserBuilder1 = createParserBuilder("com.yahoo.vespaspooler.XMLFileParser");
+ SpoolerConfig.Parsers.Builder parserBuilder2 = createParserBuilder("com.yahoo.vespaspooler.MusicFileParser");
+ LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
+ parameters.put("route", "default");
+ parameters.put("foo", "bar");
+ SpoolerConfig.Parsers.Builder parserBuilder3 = createParserBuilder("com.yahoo.vespaspooler.MusicParser",
+ parameters);
+ builder.maxfailuresize(100000).
+ maxfatalfailuresize(1000000).
+ threads(5).
+ parsers(Arrays.asList(parserBuilder1, parserBuilder2, parserBuilder3));
+ final int spoolerIndex = 0;
+ testSpoolerConfigBuilder(model, spoolerIndex, builder);
+
+ FeederConfig.Builder feederBuilder = new FeederConfig.Builder().
+ abortondocumenterror(false).
+ maxpendingbytes(8000).
+ tracelevel(7);
+ testFeederConfigBuilder(model, spoolerIndex, feederBuilder);
+ }
+
+ public void testAdvanced() throws Exception {
+ VespaModel model = createModel("src/test/cfg/clients/advancedconfig.v2");
+
+ SpoolerConfig.Builder builder = new SpoolerConfig.Builder();
+ SpoolerConfig.Parsers.Builder parserBuilder1 = createParserBuilder("com.yahoo.vespaspooler.XMLFileParser");
+ SpoolerConfig.Parsers.Builder parserBuilder2 = createParserBuilder("com.yahoo.vespaspooler.MusicFileParser");
+ LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
+ parameters.put("route", "default");
+ SpoolerConfig.Parsers.Builder parserBuilder3 = createParserBuilder("com.yahoo.vespaspooler.MusicParser",
+ parameters);
+ builder.keepsuccess(true).
+ parsers(Arrays.asList(parserBuilder1, parserBuilder2, parserBuilder3));
+ int spoolerIndex = 0;
+ testSpoolerConfigBuilder(model, spoolerIndex, builder);
+
+ FeederConfig.Builder feederBuilder = new FeederConfig.Builder().
+ abortondocumenterror(false).
+ maxpendingbytes(8000).
+ timeout(90.0);
+ testFeederConfigBuilder(model, spoolerIndex, feederBuilder);
+
+ builder = new SpoolerConfig.Builder();
+ parameters = new LinkedHashMap<>();
+ parameters.put("route", "othercluster");
+
+ parserBuilder1 = createParserBuilder("com.yahoo.vespaspooler.MusicParser",
+ parameters);
+ builder.keepsuccess(false).
+ parsers(parserBuilder1);
+ spoolerIndex = 1;
+ testSpoolerConfigBuilder(model, spoolerIndex, builder);
+
+ feederBuilder = new FeederConfig.Builder().
+ abortondocumenterror(false).
+ maxpendingbytes(4000).
+ timeout(50.0);
+ testFeederConfigBuilder(model, spoolerIndex, feederBuilder);
+
+ builder = new SpoolerConfig.Builder();
+ parserBuilder1 = new SpoolerConfig.Parsers.Builder();
+ parserBuilder1.classname("com.yahoo.vespaspooler.MusicFileParser");
+ builder.parsers(parserBuilder1);
+ String id = "plan9";
+ testSpoolerConfigBuilder(model, "clients/spoolers/" + id, builder);
+
+ feederBuilder = new FeederConfig.Builder().
+ route("myroute").
+ mbusport(14064).
+ timeout(90.0);
+ testFeederConfigBuilder(model, "clients/spoolers/" + id, feederBuilder);
+ }
+
+ SpoolerConfig.Parsers.Builder createParserBuilder(String className) {
+ return createParserBuilder(className, new HashMap<String, String>());
+ }
+
+ SpoolerConfig.Parsers.Builder createParserBuilder(String className, Map<String, String> parameters) {
+ SpoolerConfig.Parsers.Builder builder = new SpoolerConfig.Parsers.Builder();
+ builder.classname(className);
+ if (!parameters.isEmpty()) {
+ List<SpoolerConfig.Parsers.Parameters.Builder> parametersBuilders = new ArrayList<>();
+ for (Map.Entry<String, String> entry : parameters.entrySet()) {
+ final SpoolerConfig.Parsers.Parameters.Builder parametersBuilder = new SpoolerConfig.Parsers.Parameters.Builder();
+ parametersBuilder.key(entry.getKey()).value(entry.getValue());
+ parametersBuilders.add(parametersBuilder);
+ }
+ builder.parameters(parametersBuilders);
+ }
+ return builder;
+ }
+
+ private void testSpoolerConfigBuilder(VespaModel model, int index, SpoolerConfig.Builder expected) throws Exception {
+ testSpoolerConfigBuilder(model, "clients/spoolers/spooler." + index, expected);
+ }
+
+ private void testSpoolerConfigBuilder(VespaModel model, String id, SpoolerConfig.Builder expected) throws Exception {
+ SpoolerConfig.Builder b = new SpoolerConfig.Builder();
+ model.getConfig(b, id);
+ SpoolerConfig config = new SpoolerConfig(b);
+ final SpoolerConfig expectedConfig = new SpoolerConfig(expected);
+ assertEquals(expectedConfig, config);
+ }
+
+ private void testFeederConfigBuilder(VespaModel model, int index, FeederConfig.Builder expected) throws Exception {
+ testFeederConfigBuilder(model, "clients/spoolers/spooler." + index, expected);
+ }
+
+ private void testFeederConfigBuilder(VespaModel model, String id, FeederConfig.Builder expected) throws Exception {
+ FeederConfig.Builder b = new FeederConfig.Builder();
+ model.getConfig(b, id);
+ FeederConfig config = new FeederConfig(b);
+ final FeederConfig expectedConfig = new FeederConfig(expected);
+ assertEquals(expectedConfig, config);
+ }
+
+ private VespaModel createModel(String configFile) throws Exception {
+ return CommonVespaModelSetup.createVespaModelWithMusic(configFile);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java
new file mode 100755
index 00000000000..3365177409a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java
@@ -0,0 +1,192 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container;
+
+import com.yahoo.cloud.config.ClusterInfoConfig;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.cloud.config.RoutingProviderConfig;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.config.MetricDefaultsConfig;
+import com.yahoo.search.config.QrStartConfig;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.container.search.ContainerSearch;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChains;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ContainerClusterTest {
+
+ @Test
+ public void requireThatDefaultMetricConsumerFactoryCanBeConfigured() {
+ ContainerCluster cluster = newContainerCluster();
+ cluster.setDefaultMetricConsumerFactory(MetricDefaultsConfig.Factory.Enum.YAMAS_SCOREBOARD);
+ assertEquals(MetricDefaultsConfig.Factory.Enum.YAMAS_SCOREBOARD,
+ getMetricDefaultsConfig(cluster).factory());
+ }
+
+ @Test
+ public void requireThatDefaultMetricConsumerFactoryMatchesConfigDefault() {
+ ContainerCluster cluster = newContainerCluster();
+ assertEquals(new MetricDefaultsConfig(new MetricDefaultsConfig.Builder()).factory(),
+ getMetricDefaultsConfig(cluster).factory());
+ }
+
+ @Test
+ public void requireThatClusterInfoIsPopulated() {
+ ContainerCluster cluster = newContainerCluster();
+ ClusterInfoConfig config = getClusterInfoConfig(cluster);
+ assertEquals("name", config.clusterId());
+ assertEquals(2, config.nodeCount());
+ assertEquals(2, config.services().size());
+
+ Iterator<ClusterInfoConfig.Services> iterator = config.services().iterator();
+ ClusterInfoConfig.Services service = iterator.next();
+ assertEquals("host-c1", service.hostname());
+ assertEquals(0, service.index());
+ assertEquals(4, service.ports().size());
+
+ service = iterator.next();
+ assertEquals("host-c2", service.hostname());
+ assertEquals(1, service.index());
+ assertEquals(4, service.ports().size());
+ }
+
+ @Test
+ public void requreThatWeCanGetTheZoneConfig() {
+ DeployState state = new DeployState.Builder().properties(new DeployProperties.Builder().hostedVespa(true).build())
+ .zone(new Zone(Environment.test, RegionName.from("some-region"))).build();
+ MockRoot root = new MockRoot("foo", state);
+ ContainerCluster cluster = new ContainerCluster(root, "container0", "container1");
+ ConfigserverConfig.Builder builder = new ConfigserverConfig.Builder();
+ cluster.getConfig(builder);
+ ConfigserverConfig config = new ConfigserverConfig(builder);
+ assertEquals(Environment.test.value(), config.environment());
+ assertEquals("some-region", config.region());
+ }
+
+ private ContainerCluster createContainerCluster(boolean isHosted) {
+ DeployState state = new DeployState.Builder().properties(new DeployProperties.Builder().hostedVespa(isHosted).build()).build();
+ MockRoot root = new MockRoot("foo", state);
+ ContainerCluster cluster = new ContainerCluster(root, "container0", "container1");
+ cluster.setSearch(new ContainerSearch(cluster, new SearchChains(cluster, "search-chain"), new ContainerSearch.Options()));
+ return cluster;
+ }
+ private void verifyHeapSizeAsPercentageOfPhysicalMemory(boolean isHosted, int percentage) {
+ ContainerCluster cluster = createContainerCluster(isHosted);
+
+ QrStartConfig.Builder qsB = new QrStartConfig.Builder();
+ cluster.getSearch().getConfig(qsB);
+ QrStartConfig qsC= new QrStartConfig(qsB);
+ assertEquals(percentage, qsC.jvm().heapSizeAsPercentageOfPhysicalMemory());
+ }
+
+ @Test
+ public void requireThatHeapSizeAsPercentageOfPhysicalMemoryForHostedAndNot() {
+ verifyHeapSizeAsPercentageOfPhysicalMemory(true, 33);
+ verifyHeapSizeAsPercentageOfPhysicalMemory(false, 0);
+ }
+
+ private void verifyJvmArgs(boolean isHosted, boolean hasDocproc, String expectedArgs, String jvmArgs) {
+ if (isHosted && hasDocproc) {
+ String defaultHostedJVMArgs = "-XX:+UseOSErrorReporting -XX:+SuppressFatalErrorMessage";
+ if ( ! "".equals(expectedArgs)) {
+ defaultHostedJVMArgs = defaultHostedJVMArgs + " ";
+ }
+ assertEquals(defaultHostedJVMArgs + expectedArgs, jvmArgs);
+ } else {
+ assertEquals(expectedArgs, jvmArgs);
+ }
+ }
+ private void verifyJvmArgs(boolean isHosted, boolean hasDocProc) {
+ ContainerCluster cluster = createContainerCluster(isHosted);
+ if (hasDocProc) {
+ cluster.setDocproc(new ContainerDocproc(cluster, null));
+ }
+ addContainer(cluster, "c1", "host-c1");
+ assertEquals(1, cluster.getContainers().size());
+ Container container = cluster.getContainers().get(0);
+ verifyJvmArgs(isHosted, hasDocProc, "", container.getJvmArgs());
+ container.setJvmArgs("initial");
+ verifyJvmArgs(isHosted, hasDocProc, "initial", container.getJvmArgs());
+ container.prependJvmArgs("ignored");
+ verifyJvmArgs(isHosted, hasDocProc, "ignored initial", container.getJvmArgs());
+ container.appendJvmArgs("override");
+ verifyJvmArgs(isHosted, hasDocProc, "ignored initial override", container.getJvmArgs());
+ container.setJvmArgs(null);
+ verifyJvmArgs(isHosted, hasDocProc, "", container.getJvmArgs());
+ }
+ @Test
+ public void requireThatJvmArgsControlWorksForHostedAndNot() {
+ verifyJvmArgs(true, false);
+ verifyJvmArgs(true, true);
+ verifyJvmArgs(false, false);
+ verifyJvmArgs(false, true);
+ }
+
+ private void verifyThatWeCanHandleNull(boolean isHosted) {
+
+ }
+ @Test
+ public void requireThatWeCanhandleNull() {
+ ContainerCluster cluster = createContainerCluster(false);
+ addContainer(cluster, "c1", "host-c1");
+ Container container = cluster.getContainers().get(0);
+ container.setJvmArgs("");
+ String empty = container.getJvmArgs();
+ container.setJvmArgs(null);
+ assertEquals(empty, container.getJvmArgs());
+ }
+
+ @Test
+ public void requireThatRoutingProviderIsDisabledForNonHosted() {
+ DeployState state = new DeployState.Builder().properties(new DeployProperties.Builder().hostedVespa(false).build()).build();
+ MockRoot root = new MockRoot("foo", state);
+ ContainerCluster cluster = new ContainerCluster(root, "container0", "container1");
+ RoutingProviderConfig.Builder builder = new RoutingProviderConfig.Builder();
+ cluster.getConfig(builder);
+ RoutingProviderConfig config = new RoutingProviderConfig(builder);
+ assertFalse(config.enabled());
+ assertEquals(0, cluster.getAllComponents().stream().map(c -> c.getClassId().getName()).filter(c -> c.equals("com.yahoo.jdisc.http.filter.security.RoutingConfigProvider")).count());
+ }
+
+ private static void addContainer(ContainerCluster cluster, String name, String hostName) {
+ Container container = new Container(cluster, name);
+ container.setHostResource(new HostResource(new Host(null, hostName)));
+ container.initService();
+ cluster.addContainer(container);
+ }
+
+ private static ContainerCluster newContainerCluster() {
+ ContainerCluster cluster = new ContainerCluster(null, "subId", "name");
+ addContainer(cluster, "c1", "host-c1");
+ addContainer(cluster, "c2", "host-c2");
+ return cluster;
+ }
+
+ private static MetricDefaultsConfig getMetricDefaultsConfig(ContainerCluster cluster) {
+ MetricDefaultsConfig.Builder builder = new MetricDefaultsConfig.Builder();
+ cluster.getConfig(builder);
+ return new MetricDefaultsConfig(builder);
+ }
+
+ private static ClusterInfoConfig getClusterInfoConfig(ContainerCluster cluster) {
+ ClusterInfoConfig.Builder builder = new ClusterInfoConfig.Builder();
+ cluster.getConfig(builder);
+ return new ClusterInfoConfig(builder);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerIncludeTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerIncludeTest.java
new file mode 100644
index 00000000000..7173e05a4c1
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerIncludeTest.java
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container;
+
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.container.processing.ProcessingChain;
+import com.yahoo.vespa.model.container.search.searchchain.SearchChain;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.13
+ */
+public class ContainerIncludeTest {
+
+ @Test
+ public void include() {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/container/data/containerinclude/");
+ VespaModel model = creator.create();
+
+ assertThat(model.getContainerClusters().size(), is(1));
+ ContainerCluster cluster = model.getContainerClusters().values().iterator().next();
+
+ assertThat(cluster.getSearchChains(), notNullValue());
+
+ Map<String, SearchChain> searchChainMap = new HashMap<>();
+ for (SearchChain searchChain : cluster.getSearchChains().allChains().allComponents()) {
+ searchChainMap.put(searchChain.getId().stringValue(), searchChain);
+ }
+ assertThat(searchChainMap.get("searchchain1"), notNullValue());
+ assertThat(searchChainMap.get("searchchain1").getInnerComponents().size(), is(1));
+ assertThat(searchChainMap.get("searchchain1").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.Searcher1"));
+
+ assertThat(searchChainMap.get("searchchain2"), notNullValue());
+ assertThat(searchChainMap.get("searchchain2").getInnerComponents().size(), is(1));
+ assertThat(searchChainMap.get("searchchain2").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.Searcher2"));
+
+ assertThat(searchChainMap.get("searchchain3"), notNullValue());
+ assertThat(searchChainMap.get("searchchain3").getInnerComponents().size(), is(1));
+ assertThat(searchChainMap.get("searchchain3").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.Searcher3"));
+
+ assertThat(searchChainMap.get("searchchain4"), notNullValue());
+ assertThat(searchChainMap.get("searchchain4").getInnerComponents().size(), is(1));
+ assertThat(searchChainMap.get("searchchain4").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.Searcher4"));
+
+
+ assertThat(cluster.getDocprocChains(), notNullValue());
+
+ Map<String, DocprocChain> docprocChainMap = new HashMap<>();
+ for (DocprocChain docprocChain : cluster.getDocprocChains().allChains().allComponents()) {
+ docprocChainMap.put(docprocChain.getId().stringValue(), docprocChain);
+ }
+
+ assertThat(docprocChainMap.get("docprocchain1"), notNullValue());
+ assertThat(docprocChainMap.get("docprocchain1").getInnerComponents().size(), is(1));
+ assertThat(docprocChainMap.get("docprocchain1").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.DocumentProcessor1"));
+
+ assertThat(docprocChainMap.get("docprocchain2"), notNullValue());
+ assertThat(docprocChainMap.get("docprocchain2").getInnerComponents().size(), is(1));
+ assertThat(docprocChainMap.get("docprocchain2").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.DocumentProcessor2"));
+
+
+ assertThat(cluster.getProcessingChains(), notNullValue());
+
+ Map<String, ProcessingChain> processingChainMap = new HashMap<>();
+ for (ProcessingChain processingChain : cluster.getProcessingChains().allChains().allComponents()) {
+ processingChainMap.put(processingChain.getId().stringValue(), processingChain);
+ }
+
+ assertThat(processingChainMap.get("processingchain1"), notNullValue());
+ assertThat(processingChainMap.get("processingchain1").getInnerComponents().size(), is(1));
+ assertThat(processingChainMap.get("processingchain1").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.Processor1"));
+
+ assertThat(processingChainMap.get("processingchain2"), notNullValue());
+ assertThat(processingChainMap.get("processingchain2").getInnerComponents().size(), is(1));
+ assertThat(processingChainMap.get("processingchain2").getInnerComponents().iterator().next().getComponentId().stringValue(), is("com.yahoo.Processor2"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void includeNonExistent() {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/container/data/containerinclude2/");
+ creator.create();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void includeAbsolutePath() {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/container/data/containerinclude3/");
+ creator.create();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void includeNonDirectory() {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/container/data/containerinclude4/");
+ creator.create();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void include_file_with_wrong_root_element_name() {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/container/data/containerinclude5/");
+ creator.create();
+ }
+
+ @Test
+ public void include_empty_directory() {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/container/data/containerinclude6/");
+ creator.create();
+ }
+
+ @Test
+ public void included_file_with_xml_schema_violation() throws Exception {
+ try {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/container/data/include_xml_error/");
+ creator.create(true);
+ fail("Expected exception due to xml schema violation ('zearcer')");
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("XML error"));
+ assertThat(e.getMessage(), containsString("zearcer"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/ConfigserverClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/ConfigserverClusterTest.java
new file mode 100644
index 00000000000..170586bef85
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/ConfigserverClusterTest.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.configserver;
+
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.container.StatisticsConfig;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.jdisc.metrics.yamasconsumer.cloud.ScoreBoardConfig;
+import com.yahoo.net.HostName;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.container.xml.ConfigServerContainerModelBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @since 5.17
+ */
+public class ConfigserverClusterTest {
+
+ private AbstractConfigProducerRoot root;
+
+ @Before
+ public void setupCluster() {
+ String services = "<jdisc id='standalone' version='1.0'>"
+ + " <http>"
+ + " <server port='1337' id='configserver' />"
+ + " </http>"
+ + "</jdisc>";
+ root = new MockRoot();
+ new ConfigServerContainerModelBuilder(new TestOptions().rpcPort(12345).useVespaVersionInRequest(true)
+ .hostedVespa(true).environment("test").region("bar")
+ .numParallelTenantLoaders(4))
+ .build(new DeployState.Builder().build(), null, root, XML.getDocument(services).getDocumentElement());
+ root.freezeModelTopology();
+ }
+
+ @Test
+ public void testStatisticsConfig() {
+ StatisticsConfig config = root.getConfig(StatisticsConfig.class, "configserver/standalone");
+ assertThat((int) config.collectionintervalsec(), is(60));
+ assertThat((int) config.loggingintervalsec(), is(60));
+ }
+
+ @Test
+ public void testScoreBoardConfig() {
+ ScoreBoardConfig config = root.getConfig(ScoreBoardConfig.class, "configserver/standalone");
+ assertThat(config.applicationName(), is("configserver"));
+ assertThat(config.flushTime(), is(60));
+ assertThat(config.step(), is(60));
+ }
+
+ @Test
+ public void testHealthMonitorConfig() {
+ HealthMonitorConfig config = root.getConfig(HealthMonitorConfig.class, "configserver/standalone");
+ assertThat(((int) config.snapshot_interval()), is(60));
+ }
+
+ @Test
+ public void testConfigserverConfig() {
+ ConfigserverConfig config = root.getConfig(ConfigserverConfig.class, "configserver/standalone");
+ assertThat(config.configModelPluginDir().size(), is(1));
+ assertThat(config.configModelPluginDir().get(0), is(Defaults.getDefaults().vespaHome() + "lib/jars/config-models"));
+ assertThat(config.rpcport(), is(12345));
+ assertThat(config.httpport(), is(1337));
+ assertThat(config.serverId(), is(HostName.getLocalhost()));
+ assertTrue(config.useVespaVersionInRequest());
+ assertThat(config.numParallelTenantLoaders(), is(4));
+ assertFalse(config.multitenant());
+ assertTrue(config.hostedVespa());
+ assertThat(config.environment(), is("test"));
+ assertThat(config.region(), is("bar"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java b/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java
new file mode 100644
index 00000000000..b423a2b2305
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/configserver/TestOptions.java
@@ -0,0 +1,128 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.configserver;
+
+import com.yahoo.vespa.model.container.configserver.option.CloudConfigOptions;
+
+import java.util.Optional;
+
+/**
+ * @author lulf
+ * @since 5.
+ */
+public class TestOptions implements CloudConfigOptions {
+ private Optional<Integer> rpcPort = Optional.empty();
+ private Optional<String> environment = Optional.empty();
+ private Optional<String> region = Optional.empty();
+ private Optional<String> defaultFlavor = Optional.empty();
+ private Optional<String> defaultAdminFlavor = Optional.empty();
+ private Optional<String> defaultContainerFlavor = Optional.empty();
+ private Optional<String> defaultContentFlavor = Optional.empty();
+ private Optional<Boolean> useVespaVersionInRequest = Optional.empty();
+ private Optional<Boolean> hostedVespa = Optional.empty();
+ private Optional<Integer> numParallelTenantLoaders = Optional.empty();
+
+ @Override
+ public Optional<Integer> rpcPort() {
+ return rpcPort;
+ }
+
+ public TestOptions rpcPort(int port) {
+ this.rpcPort = Optional.of(port);
+ return this;
+ }
+
+ public TestOptions useVespaVersionInRequest(boolean useVespaVersionInRequest) {
+ this.useVespaVersionInRequest = Optional.of(useVespaVersionInRequest);
+ return this;
+ }
+
+ @Override
+ public Optional<Boolean> multiTenant() { return Optional.empty(); }
+
+ @Override
+ public Optional<Boolean> hostedVespa() {
+ return hostedVespa;
+ }
+
+ @Override
+ public ConfigServer[] allConfigServers() {
+ return new ConfigServer[0];
+ }
+
+ @Override
+ public Optional<Integer> zookeeperClientPort() {
+ return Optional.empty();
+ }
+
+ @Override
+ public String[] configModelPluginDirs() {
+ return new String[0];
+ }
+
+ @Override
+ public Optional<Long> sessionLifeTimeSecs() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<Long> zookeeperBarrierTimeout() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<Integer> zookeeperElectionPort() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<Integer> zookeeperQuorumPort() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<String> payloadCompressionType() { return Optional.empty(); }
+
+ @Override
+ public Optional<String> environment() { return environment; }
+
+ @Override
+ public Optional<String> region() { return region; }
+
+ @Override
+ public Optional<String> defaultFlavor() { return defaultFlavor; }
+
+ @Override
+ public Optional<String> defaultAdminFlavor() { return defaultAdminFlavor; }
+
+ @Override
+ public Optional<String> defaultContainerFlavor() { return defaultContainerFlavor; }
+
+ @Override
+ public Optional<String> defaultContentFlavor() { return defaultContentFlavor; }
+
+ @Override
+ public Optional<Boolean> useVespaVersionInRequest() { return useVespaVersionInRequest; }
+
+ @Override
+ public Optional<Integer> numParallelTenantLoaders() { return numParallelTenantLoaders; }
+
+ public TestOptions numParallelTenantLoaders(int numLoaders) {
+ this.numParallelTenantLoaders = Optional.of(numLoaders);
+ return this;
+ }
+
+ public TestOptions environment(String environment) {
+ this.environment = Optional.of(environment);
+ return this;
+ }
+
+ public TestOptions region(String region) {
+ this.region = Optional.of(region);
+ return this;
+ }
+
+ public TestOptions hostedVespa(boolean hostedVespa) {
+ this.hostedVespa = Optional.of(hostedVespa);
+ return this;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/docproc/StandaloneDocprocContainerTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/docproc/StandaloneDocprocContainerTest.java
new file mode 100644
index 00000000000..8eca10fdc9e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/docproc/StandaloneDocprocContainerTest.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.docproc;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder.Networking;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.14
+ */
+public class StandaloneDocprocContainerTest extends DomBuilderTest {
+
+ public ContainerCluster setupCluster(boolean standalone) {
+ ContainerModelBuilder builder = new ContainerModelBuilder(standalone, Networking.disable);
+ ContainerModel model = builder.build(DeployState.createTestState(), null, root, servicesXml());
+
+ if (!standalone)
+ model.getCluster().getDocproc().getChains().addServersAndClientsForChains();
+
+ root.freezeModelTopology();
+ return model.getCluster();
+ }
+
+ private Element servicesXml() {
+ return parse("" +
+ "<jdisc version=\"1.0\">\n" +
+ " <document-processing>\n" +
+ " <chain id=\"foo\">\n" +
+ " <documentprocessor id=\"MyDocproc\"/>\n" +
+ " </chain>\n" +
+ " </document-processing>\n" +
+ " <nodes>\n" +
+ " <node hostalias=\"node01\"/>\n" +
+ " </nodes>\n" +
+ "</jdisc>\n");
+ }
+
+ @Test
+ public void requireMbusProvidersWhenNonStandalone() {
+ ContainerCluster containerCluster = setupCluster(false);
+ Map<ComponentId, Component<?, ?>> components = containerCluster.getComponentsMap();
+
+ boolean foundAtLeastOneClient = false;
+ boolean foundAtLeastOneServer = false;
+
+ for (ComponentId componentId : components.keySet()) {
+ if (componentId.stringValue().contains("MbusClient")) foundAtLeastOneClient = true;
+ if (componentId.stringValue().contains("MbusServer")) foundAtLeastOneServer = true;
+ }
+ assertThat(foundAtLeastOneClient, is(true));
+ assertThat(foundAtLeastOneServer, is(true));
+
+ }
+
+ @Test
+ public void requireNoMbusProvidersWhenStandalone() {
+ ContainerCluster containerCluster = setupCluster(true);
+ Map<ComponentId, Component<?, ?>> components = containerCluster.getComponentsMap();
+
+ boolean foundAtLeastOneClient = false;
+ boolean foundAtLeastOneServer = false;
+
+ for (ComponentId componentId : components.keySet()) {
+ if (componentId.stringValue().contains("MbusClient")) foundAtLeastOneClient = true;
+ if (componentId.stringValue().contains("MbusServer")) foundAtLeastOneServer = true;
+ }
+ assertThat(foundAtLeastOneClient, is(false));
+ assertThat(foundAtLeastOneServer, is(false));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterBindingsTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterBindingsTest.java
new file mode 100644
index 00000000000..92a628120e9
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterBindingsTest.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.jdisc.config.HttpServerConfig;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.http.xml.HttpBuilder;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder.Networking;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+import static com.yahoo.collections.CollectionUtil.first;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author gjoranv
+ * @since 5.1.25
+ */
+public class FilterBindingsTest extends DomBuilderTest {
+
+ private static final String MY_CHAIN_BINDING = "http://*/my-chain-binding";
+
+ private Http buildHttp(Element xml) throws Exception {
+ Http http = new HttpBuilder().build(root, xml);
+ root.freezeModelTopology();
+ http.validate();
+ return http;
+ }
+
+
+ private void buildContainerCluster(Element containerElem) throws SAXException, IOException {
+ ContainerModel model = new ContainerModelBuilder(true, Networking.enable).build(DeployState.createTestState(), null, root, containerElem);
+ root.freezeModelTopology();
+ }
+
+ @Test
+ public void request_chain_binding_is_added_to_http() throws Exception {
+ Element xml = parse(
+ "<http>",
+ " <filtering>",
+ " <request-chain id='my-request-chain'>",
+ " <binding>" + MY_CHAIN_BINDING + "</binding>",
+ " </request-chain>",
+ " </filtering>",
+ "</http>");
+ Http http = buildHttp(xml);
+
+ Http.Binding binding = first(http.bindings);
+ assertThat(binding.filterId.getName(), is("my-request-chain"));
+ assertThat(binding.binding, is(MY_CHAIN_BINDING));
+
+ Chain<Filter> myChain = http.getFilterChains().allChains().getComponent("my-request-chain");
+ assertNotNull("Missing chain", myChain);
+ }
+
+ @Test
+ public void response_chain_binding_is_added_to_http() throws Exception {
+ Element xml = parse(
+ "<http>",
+ " <filtering>",
+ " <response-chain id='my-response-chain'>",
+ " <binding>" + MY_CHAIN_BINDING + "</binding>",
+ " </response-chain>",
+ " </filtering>",
+ "</http>");
+ Http http = buildHttp(xml);
+
+ Http.Binding binding = first(http.bindings);
+ assertThat(binding.filterId.getName(), is("my-response-chain"));
+ assertThat(binding.binding, is(MY_CHAIN_BINDING));
+
+ Chain<Filter> myChain = http.getFilterChains().allChains().getComponent("my-response-chain");
+ assertNotNull("Missing chain", myChain);
+ }
+
+ @Test
+ public void bindings_are_added_to_config_for_all_http_servers_with_jetty() throws Exception {
+ final Element xml = parse(
+ "<jdisc version='1.0' jetty='true'>",
+ " <http>",
+ " <filtering>",
+ " <request-chain id='my-request-chain'>",
+ " <binding>" + MY_CHAIN_BINDING + "</binding>",
+ " </request-chain>",
+ " </filtering>",
+ " <server id='server1' port='8000' />",
+ " <server id='server2' port='9000' />",
+ " </http>",
+ "</jdisc>");
+ buildContainerCluster(xml);
+
+ {
+ final ServerConfig config = root.getConfig(ServerConfig.class, "jdisc/http/jdisc-jetty/server1");
+ assertThat(config.filter().size(), is(1));
+ assertThat(config.filter(0).id(), is("my-request-chain"));
+ assertThat(config.filter(0).binding(), is(MY_CHAIN_BINDING));
+ }
+ {
+ final ServerConfig config = root.getConfig(ServerConfig.class, "jdisc/http/jdisc-jetty/server2");
+ assertThat(config.filter().size(), is(1));
+ assertThat(config.filter(0).id(), is("my-request-chain"));
+ assertThat(config.filter(0).binding(), is(MY_CHAIN_BINDING));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterChainsTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterChainsTest.java
new file mode 100644
index 00000000000..8966f866fb8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterChainsTest.java
@@ -0,0 +1,64 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.vespa.model.container.component.chain.Chain;
+import com.yahoo.vespa.model.container.http.xml.HttpBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import static com.yahoo.collections.CollectionUtil.first;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ * @since 5.1.26
+ */
+public class FilterChainsTest extends DomBuilderTest {
+ private Http http;
+
+ @Before
+ public void setupFilterChains() {
+ http = new HttpBuilder().build(root, servicesXml());
+ root.freezeModelTopology();
+ }
+
+ private Element servicesXml() {
+ return parse(
+ "<http>",
+ " <filtering>",
+ " <filter id='outer' />",
+ " <request-chain id='myChain'>",
+ " <filter id='inner' />",
+ " </request-chain>",
+ " </filtering>",
+ "</http>");
+ }
+
+ @Test
+ public void chains_are_built() {
+ assertNotNull(getChain("myChain"));
+ }
+
+ @Test
+ public void filters_outside_chains_are_built() {
+ Filter outerFilter = (Filter)http.getFilterChains().componentsRegistry().getComponent("outer");
+ assertNotNull(outerFilter);
+ }
+
+ @Test
+ public void filters_in_chains_are_built() {
+ Filter filter = first(getChain("myChain").getInnerComponents());
+ assertNotNull(filter);
+ assertThat(filter.getComponentId().getName(), is("inner"));
+ }
+
+ private Chain<Filter> getChain(String chainName) {
+ return http.getFilterChains().allChains().getComponent(ComponentSpecification.fromString(chainName));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterConfigTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterConfigTest.java
new file mode 100644
index 00000000000..c050b03f25e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/http/FilterConfigTest.java
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.http;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.core.http.HttpFilterConfig;
+import com.yahoo.vespa.model.container.http.xml.HttpBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import static com.yahoo.collections.CollectionUtil.first;
+import static com.yahoo.vespa.model.container.http.FilterConfigProvider.configProviderId;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ * @since 5.1.23
+ */
+public class FilterConfigTest extends DomBuilderTest {
+
+ private Http http;
+
+ @Before
+ public void setupFilterChains() {
+ http = new HttpBuilder().build(root, servicesXml());
+ root.freezeModelTopology();
+ }
+
+ private Element servicesXml() {
+ return parse(
+ "<http>",
+ " <filtering>",
+ " <filter id='no-config' />",
+
+ " <filter id='empty-config' class='EmptyConfigFilter'>",
+ " <filter-config />",
+ " </filter>",
+
+ " <filter id='config-with-params'>",
+ " <filter-config>",
+ " <key1>value1</key1>",
+ " </filter-config>",
+ " </filter>",
+
+ " <request-chain id='myChain'>",
+ " <filter id='inner-with-empty-config'>",
+ " <filter-config />",
+ " </filter>",
+ " </request-chain>",
+ " </filtering>",
+ "</http>");
+ }
+
+ @Test
+ public void filter_without_config_does_not_have_FilterConfigProvider() {
+ Filter noConfigFilter = getOuterFilter("no-config");
+
+ assertThat(getProvider(noConfigFilter), nullValue());
+ }
+
+ @Test
+ public void filterName_is_id_from_component_spec() {
+ Filter emptyConfigFilter = getOuterFilter("empty-config");
+ HttpFilterConfig config = getHttpFilterConfig(emptyConfigFilter);
+
+ assertThat(config.filterName(), is("empty-config"));
+ }
+
+ @Test
+ public void filterClass_is_class_from_component_spec() {
+ Filter emptyConfigFilter = getOuterFilter("empty-config");
+ HttpFilterConfig config = getHttpFilterConfig(emptyConfigFilter);
+
+ assertThat(config.filterClass(), is("EmptyConfigFilter"));
+ }
+
+ @Test
+ public void filter_with_empty_config_has_FilterConfigProvider_with_empty_map() {
+ Filter emptyConfigFilter = getOuterFilter("empty-config");
+ HttpFilterConfig config = getHttpFilterConfig(emptyConfigFilter);
+
+ assertThat(config.param(), is(empty()));
+ }
+
+ @Test
+ public void config_params_are_set_correctly_in_FilterConfigProvider() {
+ Filter configWithParamsFilter = getOuterFilter("config-with-params");
+ HttpFilterConfig config = getHttpFilterConfig(configWithParamsFilter);
+
+ assertThat(config.param(), hasSize(1));
+ assertThat(config.param(0).name(), is("key1"));
+ assertThat(config.param(0).value(), is("value1"));
+ }
+
+ @Test
+ public void inner_filter_can_have_filter_config() {
+ Filter innerFilter = (Filter)
+ first(http.getFilterChains().allChains().getComponent("myChain").getInnerComponents());
+
+ getHttpFilterConfig(innerFilter);
+ }
+
+ private Filter getOuterFilter(String id) {
+ return (Filter)http.getFilterChains().componentsRegistry().getComponent(id);
+ }
+
+ private static HttpFilterConfig getHttpFilterConfig(Filter filter) {
+ FilterConfigProvider configProvider = getProvider(filter);
+
+ HttpFilterConfig.Builder builder = new HttpFilterConfig.Builder();
+ configProvider.getConfig(builder);
+ return new HttpFilterConfig(builder);
+ }
+
+ static FilterConfigProvider getProvider(Filter filter) {
+ String providerId = configProviderId(filter.getComponentId()).stringValue();
+ return (FilterConfigProvider)filter.getChildren().get(providerId);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/processing/test/ProcessingChainsTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/processing/test/ProcessingChainsTest.java
new file mode 100644
index 00000000000..f57c2bb9a3c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/processing/test/ProcessingChainsTest.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.processing.test;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.vespa.model.builder.xml.dom.chains.processing.DomProcessingBuilder;
+import com.yahoo.vespa.model.container.component.chain.ChainedComponent;
+import com.yahoo.vespa.model.container.component.chain.Chains;
+import com.yahoo.vespa.model.container.processing.ProcessingChain;
+import com.yahoo.vespa.model.container.processing.Processor;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author bratseth
+ * @author gjoranv
+ */
+public class ProcessingChainsTest extends DomBuilderTest {
+
+ private Chains<ProcessingChain> processingChains;
+
+ @Before
+ public void setupProcessingChains() {
+ DomProcessingBuilder processingBuilder = new DomProcessingBuilder(null);
+ processingBuilder.build(root, servicesXml());
+ processingChains = (Chains<ProcessingChain>)root.getChildren().get("processing");
+ }
+
+ private Element servicesXml() {
+ return parse(
+ "<processing>",
+ " <processor id='processor1' class='com.yahoo.test.Processor1' />",
+ " <renderer id='renderer1' class='com.yahoo.renderer.Renderer'/>",
+ " <chain id='default'>",
+ " <processor idref='processor1'/>",
+ " <processor id='processor2' class='com.yahoo.test.Processor2'/>",
+ " </chain>",
+ "</processing>");
+ }
+
+ @Test
+ public void testProcessingChainConfiguration() {
+ ProcessingChain defaultChain = processingChains.allChains().getComponent("default");
+ assertEquals("default", defaultChain.getId().stringValue());
+ assertEquals(1, defaultChain.getInnerComponents().size());
+
+ Collection<ChainedComponent<?>> outerProcessors = processingChains.getComponentGroup().getComponents();
+ assertThat(outerProcessors.size(), is(1));
+ assertEquals("processor1", outerProcessors.iterator().next().getComponentId().toString());
+
+ Collection<Processor> innerProcessors = defaultChain.getInnerComponents();
+ assertEquals("processor2", innerProcessors.iterator().next().getComponentId().toString());
+ }
+
+ @Test
+ public void require_that_processors_have_correct_class() {
+ ChainedComponent<?> processor1 = processingChains.getComponentGroup().getComponents().iterator().next();
+ assertThat(processor1.model.bundleInstantiationSpec.classId.stringValue(),
+ is("com.yahoo.test.Processor1"));
+
+ ProcessingChain defaultChain = processingChains.allChains().getComponent("default");
+ Processor processor2 = defaultChain.getInnerComponents().iterator().next();
+ assertThat(processor2.model.bundleInstantiationSpec.classId.stringValue(),
+ is("com.yahoo.test.Processor2"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/SemanticRulesTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/SemanticRulesTest.java
new file mode 100644
index 00000000000..209e3791b16
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/SemanticRulesTest.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search;
+
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.prelude.semantics.RuleBase;
+import com.yahoo.prelude.semantics.RuleImporter;
+import com.yahoo.prelude.semantics.SemanticRulesConfig;
+import com.yahoo.prelude.semantics.parser.ParseException;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author bratseth
+ */
+public class SemanticRulesTest {
+
+ private final static String root = "src/test/java/com/yahoo/vespa/model/container/search/semanticrules";
+
+ @Test
+ public void semanticRulesTest() throws ParseException, IOException {
+ SemanticRuleBuilder ruleBuilder = new SemanticRuleBuilder();
+ SemanticRules rules = ruleBuilder.build(FilesApplicationPackage.fromFile(new File(root)));
+ SemanticRulesConfig.Builder configBuilder = new SemanticRulesConfig.Builder();
+ rules.getConfig(configBuilder);
+ SemanticRulesConfig config = new SemanticRulesConfig(configBuilder);
+ Map<String, RuleBase> ruleBases = toMap(config);
+ assertEquals(2, ruleBases.size());
+ assertTrue(ruleBases.containsKey("common"));
+ assertTrue(ruleBases.containsKey("other"));
+ assertFalse(ruleBases.get("common").isDefault());
+ assertTrue(ruleBases.get("other").isDefault());
+ }
+
+ private static Map<String, RuleBase> toMap(SemanticRulesConfig config) throws ParseException, IOException {
+ RuleImporter ruleImporter = new RuleImporter(config);
+ Map<String, RuleBase> ruleBaseMap = new HashMap<>();
+ for (SemanticRulesConfig.Rulebase ruleBaseConfig : config.rulebase()) {
+ RuleBase ruleBase = ruleImporter.importConfig(ruleBaseConfig);
+ if (ruleBaseConfig.isdefault())
+ ruleBase.setDefault(true);
+ ruleBaseMap.put(ruleBase.getName(), ruleBase);
+ }
+ return ruleBaseMap;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/Federation2Test.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/Federation2Test.java
new file mode 100644
index 00000000000..bc7197b5408
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/Federation2Test.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.test.SimpletypesConfig;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class Federation2Test extends SearchChainsTestBase {
+ @Override
+ Element servicesXml() {
+ return parse(
+ " <search>\n" +
+ "\n" +
+ " <chain id=\"chain1\">\n" +
+ " <searcher id=\"com.yahoo.example.TestSearcher\">\n" +
+ " <config name=\"test.simpletypes\">\n" +
+ " <stringval>testSearcher</stringval>\n" +
+ " </config>\n" +
+ " </searcher>\n" +
+ " </chain>\n" +
+ "\n" +
+ " <provider id=\"test-source-inherits\">\n" +
+ " <searcher id=\"com.yahoo.example.AddHitSearcher\" />\n" +
+ " <source id=\"test-inherits\" />\n" +
+ " </provider>\n" +
+ "\n" +
+ " <!-- Two providers with a common source -->\n" +
+ " <provider id=\"providerA\">\n" +
+ " <source id=\"commonSource\">\n" +
+ " <searcher id=\"com.yahoo.example.AddHitSearcher\">\n" +
+ " <config name=\"test.simpletypes\">\n" +
+ " <stringval>providerA</stringval>\n" +
+ " </config>\n" +
+ " </searcher>\n" +
+ " </source>\n" +
+ " </provider>\n" +
+ "\n" +
+ " <provider id=\"providerB\">\n" +
+ " <source idref=\"commonSource\">\n" +
+ " <searcher id=\"com.yahoo.example.AddHitSearcher\">\n" +
+ " <config name=\"test.simpletypes\">\n" +
+ " <stringval>providerB</stringval>\n" +
+ " </config>\n" +
+ " </searcher>\n" +
+ " </source>\n" +
+ " </provider>\n" +
+ "\n" +
+ " </search>\n");
+ }
+
+
+ @Test
+ public void testProviderConfigs() {
+ //SimpletypesConfig testConfig = root.getConfig(SimpletypesConfig.class, "test/searchchains/chain/chain1/component/com.yahoo.example.TestSearcher");
+ //assertEquals("testSearcher",testConfig.stringval());
+
+ SimpletypesConfig configA = root.getConfig(SimpletypesConfig.class, "searchchains/chain/providerA/source/commonSource/component/com.yahoo.example.AddHitSearcher");
+ assertEquals("providerA",configA.stringval());
+
+ SimpletypesConfig configB = root.getConfig(SimpletypesConfig.class, "searchchains/chain/providerB/source/commonSource/component/com.yahoo.example.AddHitSearcher");
+ assertEquals("providerB",configB.stringval());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/FederationTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/FederationTest.java
new file mode 100644
index 00000000000..d25af7302de
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/FederationTest.java
@@ -0,0 +1,113 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.search.federation.FederationConfig;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * Test generated config for federation.
+ * @author tonytv
+ */
+public class FederationTest extends SearchChainsTestBase {
+ @Override
+ Element servicesXml() {
+ return parse(
+ "<searchchains>",
+ " <searchchain id='federation1'>",
+ " <federation id='federationSearcher1'>",
+ " <source id='source1'>",
+ " <federationoptions optional='false' />",
+ " </source>",
+ " </federation>",
+ " </searchchain>",
+
+
+ " <provider id='provider1'>",
+ " <federationoptions optional='true' timeout='2.3 s' />",
+
+ " <source id='source1'>",
+ " <federationoptions timeout='12 ms' />",
+ " </source>",
+ " <source id='source2' />",
+ " <source id='sourceCommon' />",
+ " </provider>",
+
+ " <provider id='provider2' type='local' cluster='cluster1' />",
+
+ " <provider id='provider3'>",
+ " <source idref='sourceCommon' />",
+ " </provider>",
+
+ " <searchchain id='parentChain1' />",
+ "</searchchains>");
+ }
+
+
+ @Test
+ public void validateNativeDefaultTargets() {
+ FederationConfig.Builder fb = new FederationConfig.Builder();
+ root.getConfig(fb, "searchchains/chain/native/component/federation");
+ FederationConfig config = new FederationConfig(fb);
+
+ for (FederationConfig.Target target : config.target()) {
+ String failMessage = "Failed for target " + target.id();
+
+ if (target.id().startsWith("source")) {
+ assertTrue(failMessage, target.useByDefault());
+ } else {
+ assertFalse(failMessage, target.useByDefault());
+ }
+ }
+
+ assertThat(config.target().size(), is(5));
+ assertUseByDefault(config, "source1", false);
+ assertUseByDefault(config, "source2", false);
+
+ assertUseByDefault(config, "provider2", true);
+ assertUseByDefault(config, "cluster2", true);
+
+ assertUseByDefault(config, "sourceCommon", "provider1", false);
+ assertUseByDefault(config, "sourceCommon", "provider3", false);
+
+ }
+
+ private void assertUseByDefault(FederationConfig config, String sourceName, String providerName,
+ boolean expectedValue) {
+
+ FederationConfig.Target target = getTarget(config.target(), sourceName);
+ FederationConfig.Target.SearchChain searchChain = getProvider(target, providerName);
+ assertThat(searchChain.useByDefault(), is(expectedValue));
+ }
+
+ private FederationConfig.Target.SearchChain getProvider(FederationConfig.Target target, String providerName) {
+ for (FederationConfig.Target.SearchChain searchChain : target.searchChain()) {
+ if (searchChain.providerId().equals(providerName))
+ return searchChain;
+ }
+ fail("No provider " + providerName);
+ return null;
+ }
+
+ private void assertUseByDefault(FederationConfig config, String chainName, boolean expectedValue) {
+ FederationConfig.Target target = getTarget(config.target(), chainName);
+ assertThat(target.searchChain().size(), is(1));
+ assertThat(target.searchChain().get(0).useByDefault(), is(expectedValue));
+ }
+
+ private FederationConfig.Target getTarget(List<FederationConfig.Target> targets, String chainId) {
+ for (FederationConfig.Target target : targets) {
+ if (target.id().equals(chainId))
+ return target;
+ }
+ fail("No target with id " + chainId);
+ return null;
+ }
+
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/MockSearchClusters.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/MockSearchClusters.java
new file mode 100644
index 00000000000..daef1b845f7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/MockSearchClusters.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MockSearchClusters {
+ private static class MockSearchCluster extends AbstractSearchCluster {
+ public MockSearchCluster(AbstractConfigProducerRoot root, String clusterName, int clusterIndex, boolean isStreaming) {
+ super(root, clusterName, clusterIndex);
+ streaming = isStreaming;
+ }
+ private final boolean streaming;
+
+ @Override
+ public int getRowBits() {
+ return 0;
+ }
+
+ @Override
+ protected AbstractSearchCluster.IndexingMode getIndexingMode() { return streaming ? AbstractSearchCluster.IndexingMode.STREAMING : AbstractSearchCluster.IndexingMode.REALTIME; }
+ @Override
+ protected void assureSdConsistent() {}
+
+ @Override
+ public void getConfig(DocumentdbInfoConfig.Builder builder) {
+ }
+ @Override
+ public void getConfig(IndexInfoConfig.Builder builder) {
+ }
+ @Override
+ public void getConfig(IlscriptsConfig.Builder builder) {
+ }
+ @Override
+ public void getConfig(AttributesConfig.Builder builder) {
+ }
+ @Override
+ public void getConfig(RankProfilesConfig.Builder builder) {
+ }
+ }
+
+ public static AbstractSearchCluster mockSearchCluster(AbstractConfigProducerRoot root, String clusterName, int clusterIndex, boolean isStreaming) {
+
+ return new MockSearchCluster(root, clusterName, clusterIndex, isStreaming);
+ }
+
+ public static Map<String, AbstractSearchCluster> twoMockClusterSpecsByName(AbstractConfigProducerRoot root) {
+ Map<String, AbstractSearchCluster> result = new HashMap<>();
+ result.put("cluster1", mockSearchCluster(root, "cluster1", 1, false));
+ result.put("cluster2", mockSearchCluster(root, "cluster2", 2, true));
+ return result;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest.java
new file mode 100644
index 00000000000..09c1043734e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.container.core.ChainsConfig;
+import com.yahoo.prelude.cluster.ClusterSearcher;
+import com.yahoo.search.config.ClusterConfig;
+import com.yahoo.search.federation.ProviderConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+
+/**
+ * Test of search chains config
+ * <p>TODO: examine the actual values in the configs.</p>
+ * @author tonytv
+ */
+public class SearchChainsTest extends SearchChainsTestBase {
+ private ChainsConfig chainsConfig;
+ private ProviderConfig providerConfig;
+ private ClusterConfig clusterConfig;
+
+ @Before
+ public void subscribe() {
+ ChainsConfig.Builder chainsBuilder = new ChainsConfig.Builder();
+ chainsBuilder = (ChainsConfig.Builder)root.getConfig(chainsBuilder, "searchchains");
+ chainsConfig = new ChainsConfig(chainsBuilder);
+
+ ProviderConfig.Builder providerBuilder = new ProviderConfig.Builder();
+ providerBuilder = (ProviderConfig.Builder)root.getConfig(providerBuilder, "searchchains/chain/provider:1/component/com.yahoo.search.federation.vespa.VespaSearcher");
+ providerConfig = new ProviderConfig(providerBuilder);
+
+ ClusterConfig.Builder clusterBuilder = new ClusterConfig.Builder();
+ clusterBuilder = (ClusterConfig.Builder)root.getConfig(clusterBuilder, "searchchains/chain/cluster2/component/" + ClusterSearcher.class.getName());
+ clusterConfig = new ClusterConfig(clusterBuilder);
+ }
+
+
+ @Override
+ Element servicesXml() {
+ return parse(
+ "<searchchains>",
+ " <searcher id='searcher:1' classId='classId1' />",
+
+ " <provider id='provider:1' type='vespa' inherits='parentChain1 parentChain2' excludes='ExcludedSearcher1 ExcludedSearcher2'",
+ " cacheweight='2.3'>",
+ " <federationoptions optional='true' timeout='2.3 s' />",
+ " <nodes>",
+ " <node host='sourcehost1' port='12'/>",
+ " <node host='sourcehost2' port='34'/>",
+ " </nodes>",
+
+ " <source id='source:1' inherits='parentChain3 parentChain4' excludes='ExcludedSearcher3 ExcludedSearcher4'>",
+ " <federationoptions timeout='12 ms' />",
+ " </source>",
+ " <source id='source:2' />",
+ " </provider>",
+
+ " <provider id='provider:2' type='local' cluster='cluster1' />",
+ " <provider id='provider:3' />",
+
+ " <provider id='vespa-provider' type='vespa' >",
+ " <nodes>",
+ " <node host='localhost' port='" + Defaults.getDefaults().vespaWebServicePort() + "' />",
+ " </nodes>",
+ " <config name='search.federation.provider'>",
+ " <queryType>PROGRAMMATIC</queryType>",
+ " </config>",
+ " </provider>",
+
+ " <searchchain id='default:99'>",
+ " <federation id='federation:98' provides='provide_federation' before='p1 p2' after='s1 s2'>",
+ " <source id='source:1'>",
+ " <federationoptions optional='false' />",
+ " </source>",
+ " </federation>",
+ " </searchchain>",
+
+ " <searchchain id='parentChain1' />",
+ " <searchchain id='parentChain2' />",
+ " <searchchain id='parentChain3' />",
+ " <searchchain id='parentChain4' />",
+ "</searchchains>");
+ }
+
+ @Test
+ public void require_vespa_searcher_inside_vespa_provider() {
+ SearchChains searchchains = getSearchChains();
+ SearchChain vespaProvider = searchchains.allChains().getComponent("vespa-provider");
+ Searcher<?> vespaSearcher = vespaProvider.getInnerComponents().iterator().next();
+ assertThat(vespaSearcher, instanceOf(HttpProviderSearcher.class));
+ }
+
+ private SearchChains getSearchChains() {
+ return (SearchChains) root.getChildren().get("searchchains");
+ }
+
+ @Test
+ public void require_user_config_for_vespa_searcher_works() {
+ assertEquals(root.getConfig(ProviderConfig.class, "searchchains/chain/vespa-provider/component/com.yahoo.search.federation.vespa.VespaSearcher").
+ queryType(), ProviderConfig.QueryType.PROGRAMMATIC);
+ }
+
+ @Test
+ public void require_that_source_chain_spec_id_is_namespaced_in_provider_id() {
+ Source source = (Source) getSearchChains().allChains().getComponent("source:1@provider:1");
+ assertThat(source.getChainSpecification().componentId.getNamespace(), is(ComponentId.fromString("provider:1")));
+ }
+
+ @Test
+ public void validateHttpProviderConfig() {
+ assertNotNull(providerConfig);
+ }
+
+ @Test
+ public void validateLocalProviderConfig() {
+ assertEquals(2, clusterConfig.clusterId());
+ assertEquals("cluster2", clusterConfig.clusterName());
+ }
+
+ public static boolean verifyChainExists(List<ChainsConfig.Chains> chains, String componentId) {
+ for (ChainsConfig.Chains c : chains) {
+ if (c.id().equals(componentId)) return true;
+ }
+ return false;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest2.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest2.java
new file mode 100644
index 00000000000..5e93233f443
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTest2.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.model.builder.xml.dom.chains.search.DomSearchChainsBuilder;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilderTest;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.hamcrest.Matchers.containsString;
+
+/**
+ * @author gjoranv
+ * @since 5.1.11
+ */
+public class SearchChainsTest2 {
+
+ private MockRoot root;
+
+ @Before
+ public void prepareTest() throws Exception {
+ root = new MockRoot("root");
+ }
+
+ @Test
+ public void fail_upon_unresolved_inheritance() {
+ final Element searchElem = DomBuilderTest.parse(
+ "<search>",
+ " <chain id='default' inherits='nonexistent' />",
+ "</search>");
+ try {
+ SearchChains chains = new DomSearchChainsBuilder().build(new MockRoot(), searchElem);
+ chains.validate();
+ fail("Expected exception when inheriting a nonexistent search chain.");
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Missing chain 'nonexistent'"));
+ }
+ }
+
+ @Test
+ public void fail_upon_two_user_declared_chains_with_same_name() {
+ final Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0'>",
+ ContainerModelBuilderTest.nodesXml,
+ " <search>",
+ " <chain id='same' />",
+ " <chain id='same' />",
+ " </search>",
+ "</jdisc>");
+ try {
+ ContainerModelBuilderTest.createModel(root, clusterElem);
+ fail("Expected exception when declaring chains with duplicate id.");
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Two entities have the same component id 'same'"));
+ }
+ }
+
+ @Test
+ public void fail_upon_user_declared_chain_with_same_id_as_builtin_chain() throws Exception {
+ final Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0'>",
+ ContainerModelBuilderTest.nodesXml,
+ " <search>",
+ " <chain id='vespa' />",
+ " </search>",
+ "</jdisc>");
+ try {
+ ContainerModelBuilderTest.createModel(root, clusterElem);
+ fail("Expected exception when taking the id from a builtin chain.");
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Two entities have the same component id 'vespa'"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTestBase.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTestBase.java
new file mode 100644
index 00000000000..a942e428be0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SearchChainsTestBase.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.binaryprefix.BinaryPrefix;
+import com.yahoo.binaryprefix.BinaryScaledAmount;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.vespa.model.builder.xml.dom.chains.search.DomSearchChainsBuilder;
+import org.junit.Before;
+import org.w3c.dom.Element;
+
+/** Creates SearchChains model from xml input.
+ * @author tonytv
+ */
+public abstract class SearchChainsTestBase extends DomBuilderTest {
+
+ @Before
+ public void setupSearchChains() {
+ SearchChains searchChains = new DomSearchChainsBuilder().build(root, servicesXml());
+ searchChains.initialize(MockSearchClusters.twoMockClusterSpecsByName(root),
+ new BinaryScaledAmount(100, BinaryPrefix.mega));
+ root.freezeModelTopology();
+ }
+
+ abstract Element servicesXml();
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupTest.java
new file mode 100644
index 00000000000..bf50a40fd2d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/searchchain/SourceGroupTest.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.search.searchchain.model.federation.FederationOptions;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.hamcrest.Matchers.containsString;
+
+/**
+ * @author tonytv
+ */
+public class SourceGroupTest {
+ private MockRoot root;
+ private SearchChains searchChains;
+
+ @Before
+ public void setUp() throws Exception {
+ root = new MockRoot();
+ searchChains = new SearchChains(root, "searchchains");
+ }
+
+ @Test
+ public void report_error_when_no_leader() {
+ try {
+ Provider provider = createProvider("p1");
+ Source source = createSource("s1", Source.GroupOption.participant);
+ provider.addSource(source);
+
+ searchChains.add(provider);
+ root.freezeModelTopology();
+
+ searchChains.validate();
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Missing leader for the source s1."));
+ return;
+ }
+ fail("Expected exception");
+ }
+
+ private Provider createProvider(String p1) {
+ return new Provider(createSearchChainSpecification(p1), new FederationOptions());
+ }
+
+ private ChainSpecification createSearchChainSpecification(String id) {
+ return new ChainSpecification(ComponentId.fromString(id),
+ new ChainSpecification.Inheritance(null, null),
+ Collections.<Phase>emptyList(),
+ Collections.<ComponentSpecification>emptySet());
+ }
+
+ private Source createSource(String sourceId, Source.GroupOption groupOption) {
+ return new Source(
+ createSearchChainSpecification(sourceId),
+ new FederationOptions(),
+ groupOption);
+ }
+
+ @Test
+ public void require_that_source_and_provider_id_is_not_allowed_to_be_equal() {
+ Provider provider = createProvider("sameId");
+ Provider provider2 = createProvider("ignoredId");
+
+ Source source = createSource("sameId", Source.GroupOption.leader);
+
+ provider2.addSource(source);
+
+ searchChains.add(provider);
+ searchChains.add(provider2);
+ root.freezeModelTopology();
+
+ try {
+ searchChains.validate();
+ fail("Expected exception");
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Same id used for a source"));
+ assertThat(e.getMessage(), containsString("'sameId'"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/common.sr b/config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/common.sr
new file mode 100644
index 00000000000..d059d3c2de6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/common.sr
@@ -0,0 +1,21 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+## Some test rules
+
+# Spelling correction
+bahc -> bach;
+
+# Stopwords
+somelongstopword -> ;
+[stopword] -> ;
+[stopword] :- someotherlongstopword, yetanotherstopword;
+
+
+[song] by [artist] -> song:[song] artist:[artist];
+
+[song] :- together, imagine, tinseltown;
+[artist] :- youngbloods, beatles, zappa;
+
+# Negative
+various +> -kingz;
+
+
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/other.sr b/config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/other.sr
new file mode 100644
index 00000000000..b57097287ce
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/semanticrules/rules/other.sr
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@default
+@include(common.sr)
+
+[stopword] -> ;
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/PageTemplatesTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/PageTemplatesTestCase.java
new file mode 100644
index 00000000000..4e8b691518a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/PageTemplatesTestCase.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.test;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.io.IOUtils;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.vespa.model.container.search.PageTemplates;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author bratseth
+ */
+public class PageTemplatesTestCase extends junit.framework.TestCase {
+
+ private final static String root="src/test/java/com/yahoo/vespa/model/container/search/test/pages";
+
+ public void testExport() throws IOException {
+ List<NamedReader> pageFiles=new ArrayList<>(2);
+ pageFiles.add(new NamedReader(root + "/slottingSerp.xml", IOUtils.createReader(root + "/slottingSerp.xml")));
+ pageFiles.add(new NamedReader(root + "/richSerp.xml", IOUtils.createReader(root + "/richSerp.xml")));
+ pageFiles.add(new NamedReader(root + "/footer.xml", IOUtils.createReader(root + "/footer.xml")));
+ pageFiles.add(new NamedReader(root + "/richerSerp.xml", IOUtils.createReader(root + "/richerSerp.xml")));
+ pageFiles.add(new NamedReader(root + "/header.xml", IOUtils.createReader(root + "/header.xml")));
+ assertEquals(IOUtils.readFile(new File(root, "/pages.cfg")), StringUtilities.implodeMultiline(ConfigInstance.serialize(new PageTemplates(pageFiles).getConfig())));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfileVariantsTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfileVariantsTestCase.java
new file mode 100644
index 00000000000..6761bd9bef7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfileVariantsTestCase.java
@@ -0,0 +1,105 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.test;
+
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.config.QueryProfileXMLReader;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import java.io.IOException;
+
+import static helpers.CompareConfigTestHelper.assertSerializedConfigFileEquals;
+
+/**
+ * @author bratseth
+ */
+public class QueryProfileVariantsTestCase extends junit.framework.TestCase {
+
+ private final String root = "src/test/java/com/yahoo/vespa/model/container/search/test/";
+
+ public void testConfigCreation() throws IOException {
+ QueryProfileRegistry registry = new QueryProfileXMLReader().read(root + "queryprofilevariants");
+ QueryProfiles profiles = new QueryProfiles(registry);
+ assertSerializedConfigFileEquals(root + "query-profile-variants-configuration.cfg", profiles.getConfig().toString());
+ }
+
+ public void testConfigCreation2() throws IOException {
+ QueryProfileRegistry registry = new QueryProfileXMLReader().read("src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2");
+ QueryProfiles profiles = new QueryProfiles(registry);
+ assertSerializedConfigFileEquals(root + "query-profile-variants2-configuration.cfg", profiles.getConfig().toString());
+ }
+
+ public void testConfigCreationNewsBESimple() throws IOException {
+ QueryProfileRegistry registry = new QueryProfileXMLReader().read(root + "newsbesimple");
+ QueryProfiles profiles = new QueryProfiles(registry);
+ assertSerializedConfigFileEquals(root + "newsbe-query-profiles-simple.cfg", profiles.getConfig().toString());
+ }
+
+ public void testConfigCreationNewsFESimple() throws IOException {
+ QueryProfileRegistry registry = new QueryProfileXMLReader().read(root + "newsfesimple");
+ QueryProfiles profiles = new QueryProfiles(registry);
+ assertSerializedConfigFileEquals(root + "newsfe-query-profiles-simple.cfg", profiles.getConfig().toString());
+ }
+
+ public void testVariantsOfExplicitCompound() throws IOException {
+ QueryProfileRegistry registry = new QueryProfileRegistry();
+
+ QueryProfile a1 = new QueryProfile("a1");
+ a1.set("b", "a1.b", registry);
+
+ QueryProfile profile = new QueryProfile("test");
+ profile.setDimensions(new String[] {"x"});
+ profile.set("a", a1, registry);
+ profile.set("a.b", "a.b.x1", new String[] {"x1"}, registry);
+ profile.set("a.b", "a.b.x2", new String[] {"x2"}, registry);
+
+ registry.register(a1);
+ registry.register(profile);
+
+ QueryProfiles profiles = new QueryProfiles(registry);
+ assertSerializedConfigFileEquals(root + "variants-of-explicit-compound.cfg", profiles.getConfig().toString());
+ }
+
+ public void testVariantsOfExplicitCompoundWithVariantReference() throws IOException {
+ QueryProfileRegistry registry = new QueryProfileRegistry();
+
+ QueryProfile a1 = new QueryProfile("a1");
+ a1.set("b", "a1.b", registry);
+
+ QueryProfile a2 = new QueryProfile("a2");
+ a2.set("b", "a2.b", registry);
+
+ QueryProfile profile = new QueryProfile("test");
+ profile.setDimensions(new String[] {"x"});
+ profile.set("a", a1, registry);
+ profile.set("a", a2, new String[] {"x1"}, registry);
+ profile.set("a.b", "a.b.x1", new String[] {"x1"}, registry);
+ profile.set("a.b", "a.b.x2", new String[] {"x2"}, registry);
+
+ registry.register(a1);
+ registry.register(a2);
+ registry.register(profile);
+
+ QueryProfiles profiles = new QueryProfiles(registry);
+ assertSerializedConfigFileEquals(root + "variants-of-explicit-compound-with-reference.cfg", profiles.getConfig().toString());
+ }
+
+ /** For comparison with the above */
+ public void testExplicitReferenceOverride() throws IOException {
+ QueryProfileRegistry registry = new QueryProfileRegistry();
+
+ QueryProfile a1 = new QueryProfile("a1");
+ a1.set("b", "a1.b", registry);
+
+ QueryProfile profile = new QueryProfile("test");
+ profile.set("a", a1, registry);
+ profile.set("a.b", "a.b", registry);
+ assertEquals("a.b", profile.get("a.b"));
+
+ registry.register(a1);
+ registry.register(profile);
+
+ QueryProfiles profiles = new QueryProfiles(registry);
+ assertSerializedConfigFileEquals(root + "explicit-reference-override.cfg", profiles.getConfig().toString());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfilesTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfilesTestCase.java
new file mode 100644
index 00000000000..b9ec84c389b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/QueryProfilesTestCase.java
@@ -0,0 +1,122 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.test;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.search.query.profile.QueryProfile;
+import com.yahoo.search.query.profile.QueryProfileRegistry;
+import com.yahoo.search.query.profile.compiled.CompiledQueryProfileRegistry;
+import com.yahoo.search.query.profile.config.QueryProfileConfigurer;
+import com.yahoo.search.query.profile.config.QueryProfileXMLReader;
+import com.yahoo.search.query.profile.types.FieldDescription;
+import com.yahoo.search.query.profile.types.FieldType;
+import com.yahoo.search.query.profile.types.QueryProfileType;
+import com.yahoo.search.query.profile.types.QueryProfileTypeRegistry;
+import com.yahoo.vespa.model.container.search.QueryProfiles;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static helpers.CompareConfigTestHelper.assertSerializedConfigFileEquals;
+
+/**
+ * Tests generation of config from query profiles (XML reading is tested elsewhere)
+ *
+ * @author bratseth
+ */
+public class QueryProfilesTestCase extends junit.framework.TestCase {
+
+ private final static String root="src/test/java/com/yahoo/vespa/model/container/search/test/";
+
+ public void testEmpty() throws IOException {
+ QueryProfileRegistry reg = new QueryProfileRegistry();
+ assertConfig("empty.cfg", reg);
+ }
+
+ public void testQueryProfiles() throws IOException {
+ final boolean mandatory=true;
+ final boolean overridable=true;
+ QueryProfileRegistry registry=new QueryProfileRegistry();
+ QueryProfileTypeRegistry typeRegistry=registry.getTypeRegistry();
+
+ QueryProfileType userType=new QueryProfileType("user");
+ userType.setStrict(true);
+ userType.addField(new FieldDescription("robot", FieldType.fromString("boolean",typeRegistry), "machine automaton", mandatory, !overridable));
+ userType.addField(new FieldDescription("ads", FieldType.fromString("string",typeRegistry), mandatory, overridable));
+ userType.addField(new FieldDescription("age", FieldType.fromString("integer",typeRegistry), !mandatory, overridable));
+ typeRegistry.register(userType);
+
+ QueryProfileType rootType=new QueryProfileType("root");
+ QueryProfileType nativeProfile=typeRegistry.getComponent("native");
+ assertNotNull(nativeProfile);
+ assertTrue(nativeProfile.isBuiltin());
+ rootType.inherited().add(nativeProfile);
+ rootType.setMatchAsPath(true);
+ rootType.addField(new FieldDescription("user", FieldType.fromString("query-profile:user",typeRegistry), mandatory, overridable));
+ typeRegistry.register(rootType);
+
+ QueryProfileType marketType=new QueryProfileType("market");
+ marketType.inherited().add(rootType);
+ marketType.addField(new FieldDescription("market", FieldType.fromString("string",typeRegistry), !mandatory, !overridable));
+ typeRegistry.register(marketType);
+
+ QueryProfile defaultProfile=new QueryProfile("default");
+ defaultProfile.set("ranking","production23", registry);
+ defaultProfile.set("representation.defaultIndex", "title", registry);
+ defaultProfile.setOverridable("representation.defaultIndex", false, null);
+ registry.register(defaultProfile);
+
+ QueryProfile test=new QueryProfile("test");
+ test.set("tracelevel",2,registry);
+ registry.register(test);
+
+ QueryProfile genericUser=new QueryProfile("genericUser");
+ genericUser.setType(userType);
+ genericUser.set("robot",false,registry);
+ genericUser.set("ads","all",registry);
+ registry.register(genericUser);
+
+ QueryProfile root=new QueryProfile("root");
+ root.setType(rootType);
+ root.addInherited(defaultProfile);
+ root.addInherited(test);
+ root.set("hits",30,registry);
+ root.setOverridable("hits",false,null);
+ root.set("unique","category",registry);
+ root.set("user",genericUser,registry);
+ root.set("defaultage", "7d",registry);
+ registry.register(root);
+
+ QueryProfile marketUser=new QueryProfile("marketUser");
+ marketUser.setType(userType);
+ marketUser.addInherited(genericUser);
+ marketUser.set("ads","none",registry);
+ marketUser.set("age",25,registry);
+ registry.register(marketUser);
+
+ QueryProfile market=new QueryProfile("root/market");
+ market.setType(marketType);
+ market.addInherited(root);
+ market.set("hits",15,registry);
+ market.set("user",marketUser,registry);
+ market.set("market","some market",registry);
+ market.set("marketHeading","Market of %{market}",registry);
+ registry.register(market);
+
+ QueryProfile untypedUser=new QueryProfile("untypedUser");
+ untypedUser.set("robot",false,registry);
+ untypedUser.set("robot.type","continent-class",registry);
+ registry.register(untypedUser);
+
+ assertConfig("query-profiles.cfg",registry);
+ }
+
+ protected void assertConfig(String correctFileName, QueryProfileRegistry check) throws IOException {
+ assertSerializedConfigFileEquals(root + "/" + correctFileName,
+ com.yahoo.text.StringUtilities.implodeMultiline(com.yahoo.config.ConfigInstance.serialize(new QueryProfiles(check).getConfig())));
+
+ // Also assert that the correct config config can actually be read as a config source
+ QueryProfileConfigurer configurer = new QueryProfileConfigurer("file:" + root + "empty.cfg");
+ configurer.shutdown();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/empty.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/empty.cfg
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/empty.cfg
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/explicit-reference-override.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/explicit-reference-override.cfg
new file mode 100644
index 00000000000..99e2e3c5dcb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/explicit-reference-override.cfg
@@ -0,0 +1,13 @@
+queryprofile[0].id "a1"
+queryprofile[0].type ""
+queryprofile[0].property[0].name "b"
+queryprofile[0].property[0].value "a1.b"
+queryprofile[0].property[0].overridable ""
+queryprofile[1].id "test"
+queryprofile[1].type ""
+queryprofile[1].property[0].name "a.b"
+queryprofile[1].property[0].value "a.b"
+queryprofile[1].property[0].overridable ""
+queryprofile[1].reference[0].name "a"
+queryprofile[1].reference[0].value "a1"
+queryprofile[1].reference[0].overridable "" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbe-query-profiles-simple.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbe-query-profiles-simple.cfg
new file mode 100644
index 00000000000..196b6c3513a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbe-query-profiles-simple.cfg
@@ -0,0 +1,19 @@
+queryprofile[0].id "scthumbnail"
+queryprofile[0].type ""
+queryprofile[0].dimensions[0] "custid_1"
+queryprofile[0].dimensions[1] "custid_2"
+queryprofile[0].dimensions[2] "custid_3"
+queryprofile[0].dimensions[3] "custid_4"
+queryprofile[0].dimensions[4] "custid_5"
+queryprofile[0].dimensions[5] "custid_6"
+queryprofile[0].property[0].name "debug.query.profile.file.scthumbnail.xml"
+queryprofile[0].property[0].value ""
+queryprofile[0].property[0].overridable ""
+queryprofile[0].property[1].name "scthumbnail.activate"
+queryprofile[0].property[1].value "true"
+queryprofile[0].property[1].overridable "true"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[0] "yahoo"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[1] "uk"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[2] "sc"
+queryprofile[0].queryprofilevariant[0].property[0].name "scthumbnail.sourcecountry"
+queryprofile[0].queryprofilevariant[0].property[0].value "uk" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbesimple/scthumbnail.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbesimple/scthumbnail.xml
new file mode 100644
index 00000000000..f3d1e6892ba
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsbesimple/scthumbnail.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="scthumbnail">
+ <dimensions>custid_1,custid_2,custid_3,custid_4,custid_5,custid_6</dimensions>
+
+ <field name="debug.query.profile.file.scthumbnail.xml"/>
+ <field name="scthumbnail.activate" overridable="true">true</field>
+
+ <!-- Set the source country used for the thumb query, default US -->
+ <query-profile for="yahoo,uk,sc">
+ <field name="scthumbnail.sourcecountry">uk</field>
+ </query-profile>
+</query-profile>
+
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfe-query-profiles-simple.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfe-query-profiles-simple.cfg
new file mode 100644
index 00000000000..461f9b606c6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfe-query-profiles-simple.cfg
@@ -0,0 +1,26 @@
+queryprofile[0].id "backend/news"
+queryprofile[0].type ""
+queryprofile[0].dimensions[0] "vertical"
+queryprofile[0].dimensions[1] "sort"
+queryprofile[0].dimensions[2] "offset"
+queryprofile[0].dimensions[3] "resulttypes"
+queryprofile[0].dimensions[4] "rss"
+queryprofile[0].dimensions[5] "age"
+queryprofile[0].dimensions[6] "intl"
+queryprofile[0].dimensions[7] "testid"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[0] "news"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[1] "*"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[2] "*"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[3] "article"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[4] "0"
+queryprofile[0].queryprofilevariant[0].property[0].name "discovery"
+queryprofile[0].queryprofilevariant[0].property[0].value "sources"
+queryprofile[0].queryprofilevariant[0].property[1].name "discovery.sources.count"
+queryprofile[0].queryprofilevariant[0].property[1].value "13"
+queryprofile[0].queryprofilevariant[0].property[2].name "discoverytypes"
+queryprofile[0].queryprofilevariant[0].property[2].value "article"
+queryprofile[1].id "default"
+queryprofile[1].type ""
+queryprofile[1].reference[0].name "source.news"
+queryprofile[1].reference[0].value "backend/news"
+queryprofile[1].reference[0].overridable "" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/backend_news.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/backend_news.xml
new file mode 100644
index 00000000000..3585ccd5eda
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/backend_news.xml
@@ -0,0 +1,9 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="backend/news">
+ <dimensions>vertical,sort,offset,resulttypes,rss,age,intl,testid</dimensions>
+ <query-profile for="news,*,*,article,0">
+ <field name="discovery">sources</field>
+ <field name="discoverytypes">article</field>
+ <field name="discovery.sources.count">13</field>
+ </query-profile>
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/default.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/default.xml
new file mode 100644
index 00000000000..d8dbe6e929a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/newsfesimple/default.xml
@@ -0,0 +1,4 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="default">
+ <field name="source.news"><ref>backend/news</ref></field>
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/footer.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/footer.xml
new file mode 100644
index 00000000000..0866aaaa583
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/footer.xml
@@ -0,0 +1,5 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<page id="footer">
+ <section layout="row" source="popularSearches"/>
+ <section id="extraFooter" layout="row" source="topArticles"/>
+</page>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/header.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/header.xml
new file mode 100644
index 00000000000..a894e8b9a3e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/header.xml
@@ -0,0 +1,7 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<page id="header">
+ <section layout="row">
+ <section source="global"/>
+ <section source="notifications"/>
+ </section>
+</page>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/pages.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/pages.cfg
new file mode 100644
index 00000000000..a65903d052b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/pages.cfg
@@ -0,0 +1,5 @@
+page[0] "<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->\n<page id=\"slottingSerp\" layout=\"mainAndRight\">\n <section layout=\"column\" placement=\"main\" source=\"*\" blending=\"slot\"/>\n <section layout=\"column\" placement=\"right\" source=\"ads\"/>\n</page>\n"
+page[1] "<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->\n<page id=\"richSerp\" layout=\"mainAndRight\">\n <section layout=\"row\" placement=\"main\">\n <section layout=\"column\" description=\"left main pane\">\n <section layout=\"row\" max=\"5\" description=\"Bar of images, from one of two possible sources\">\n <choice>\n <source name=\"images\"/>\n <source name=\"flickr\"/>\n </choice>\n </section>\n <section max=\"1\" source=\"local map video ticker weather\" description=\"A single relevant graphically rich element\"/>\n <section blending=\"blend\" max=\"10\" source=\"web news\" description=\"Various kinds of traditional search results\"/>\n </section>\n <section layout=\"column\" blending=\"group\" source=\"answers blogs twitter\" description=\"right main pane, ugc stuff, grouped by source\"/>\n </section>\n <section layout=\"column\" source=\"ads\" placement=\"right\"/>\n</page>\n"
+page[2] "<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->\n<page id=\"footer\">\n <section layout=\"row\" source=\"popularSearches\"/>\n <section id=\"extraFooter\" layout=\"row\" source=\"topArticles\"/>\n</page>\n"
+page[3] "<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->\n<page id=\"richerSerp\" layout=\"column\">\n <include idref=\"header\"/>\n <section layout=\"mainAndRight\">\n <section layout=\"row\" placement=\"main\">\n <section layout=\"column\" description=\"left main pane\">\n <choice>\n <alternative>\n <section layout=\"row\" max=\"5\" description=\"Bar of images, from one of two possible sources\">\n <choice>\n <source name=\"images\"/>\n <alternative>\n <source name=\"flickr\">\n <presentation name=\"mouseOverImage\"/>\n </source>\n <source name=\"twitpic\">\n <choice>\n <presentation name=\"mouseOverImage\">\n <parameter name=\"hovertime\">5</parameter>\n <parameter name=\"borderColor\">#ff00ff</parameter>\n </presentation>\n <presentation name=\"regularImage\"/>\n </choice>\n <parameter name=\"filter\">origin=twitter</parameter>\n </source>\n </alternative>\n </choice>\n <choice>\n <presentation name=\"regularImageBox\"/>\n <presentation name=\"newImageBox\"/>\n </choice>\n </section>\n <section max=\"1\" source=\"local map video ticker weather\" description=\"A single relevant graphically rich element\"/>\n </alternative>\n <section blending=\"blend\" max=\"10\" source=\"web news\" description=\"Various kinds of traditional search results\"/>\n </choice>\n </section>\n <section layout=\"column\" blending=\"group\" source=\"answers blogs twitter\" description=\"right main pane, ugc stuff, grouped by source\"/>\n </section>\n <section layout=\"column\" source=\"ads\" placement=\"right\" order=\"relevance clickProbability\">\n <presentation name=\"newAdBox\"/>\n </section>\n </section>\n <include idref=\"footer\"/>\n</page>\n"
+page[4] "<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->\n<page id=\"header\">\n <section layout=\"row\">\n <section source=\"global\"/>\n <section source=\"notifications\"/>\n </section>\n</page>\n" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richSerp.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richSerp.xml
new file mode 100644
index 00000000000..f47b57d699e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richSerp.xml
@@ -0,0 +1,17 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<page id="richSerp" layout="mainAndRight">
+ <section layout="row" placement="main">
+ <section layout="column" description="left main pane">
+ <section layout="row" max="5" description="Bar of images, from one of two possible sources">
+ <choice>
+ <source name="images"/>
+ <source name="flickr"/>
+ </choice>
+ </section>
+ <section max="1" source="local map video ticker weather" description="A single relevant graphically rich element"/>
+ <section blending="blend" max="10" source="web news" description="Various kinds of traditional search results"/>
+ </section>
+ <section layout="column" blending="group" source="answers blogs twitter" description="right main pane, ugc stuff, grouped by source"/>
+ </section>
+ <section layout="column" source="ads" placement="right"/>
+</page>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richerSerp.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richerSerp.xml
new file mode 100644
index 00000000000..e2206c0f288
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/richerSerp.xml
@@ -0,0 +1,45 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<page id="richerSerp" layout="column">
+ <include idref="header"/>
+ <section layout="mainAndRight">
+ <section layout="row" placement="main">
+ <section layout="column" description="left main pane">
+ <choice>
+ <alternative>
+ <section layout="row" max="5" description="Bar of images, from one of two possible sources">
+ <choice>
+ <source name="images"/>
+ <alternative>
+ <source name="flickr">
+ <presentation name="mouseOverImage"/>
+ </source>
+ <source name="twitpic">
+ <choice>
+ <presentation name="mouseOverImage">
+ <parameter name="hovertime">5</parameter>
+ <parameter name="borderColor">#ff00ff</parameter>
+ </presentation>
+ <presentation name="regularImage"/>
+ </choice>
+ <parameter name="filter">origin=twitter</parameter>
+ </source>
+ </alternative>
+ </choice>
+ <choice>
+ <presentation name="regularImageBox"/>
+ <presentation name="newImageBox"/>
+ </choice>
+ </section>
+ <section max="1" source="local map video ticker weather" description="A single relevant graphically rich element"/>
+ </alternative>
+ <section blending="blend" max="10" source="web news" description="Various kinds of traditional search results"/>
+ </choice>
+ </section>
+ <section layout="column" blending="group" source="answers blogs twitter" description="right main pane, ugc stuff, grouped by source"/>
+ </section>
+ <section layout="column" source="ads" placement="right" order="relevance clickProbability">
+ <presentation name="newAdBox"/>
+ </section>
+ </section>
+ <include idref="footer"/>
+</page>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/slottingSerp.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/slottingSerp.xml
new file mode 100644
index 00000000000..8e40909a489
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/pages/slottingSerp.xml
@@ -0,0 +1,5 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<page id="slottingSerp" layout="mainAndRight">
+ <section layout="column" placement="main" source="*" blending="slot"/>
+ <section layout="column" placement="right" source="ads"/>
+</page>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants-configuration.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants-configuration.cfg
new file mode 100644
index 00000000000..c86bba23286
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants-configuration.cfg
@@ -0,0 +1,41 @@
+queryprofile[0].id "variants1"
+queryprofile[0].type ""
+queryprofile[0].dimensions[0] "x"
+queryprofile[0].dimensions[1] "y"
+queryprofile[0].property[0].name "a"
+queryprofile[0].property[0].value "a-deflt"
+queryprofile[0].property[0].overridable ""
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[0] "x1"
+queryprofile[0].queryprofilevariant[0].fordimensionvalues[1] "y1"
+queryprofile[0].queryprofilevariant[0].inherit[0] "variants2"
+queryprofile[0].queryprofilevariant[0].inherit[1] "wparent2"
+queryprofile[0].queryprofilevariant[0].property[0].name "a"
+queryprofile[0].queryprofilevariant[0].property[0].value "x1.y1.a"
+queryprofile[0].queryprofilevariant[0].property[1].name "b"
+queryprofile[0].queryprofilevariant[0].property[1].value "x1.y1.b"
+queryprofile[0].queryprofilevariant[1].fordimensionvalues[0] "x1"
+queryprofile[0].queryprofilevariant[1].property[0].name "a"
+queryprofile[0].queryprofilevariant[1].property[0].value "x1.y?.a"
+queryprofile[0].queryprofilevariant[2].fordimensionvalues[0] "*"
+queryprofile[0].queryprofilevariant[2].fordimensionvalues[1] "y1"
+queryprofile[0].queryprofilevariant[2].property[0].name "a"
+queryprofile[0].queryprofilevariant[2].property[0].value "x?.y1.a"
+queryprofile[0].queryprofilevariant[2].reference[0].name "toVariants2"
+queryprofile[0].queryprofilevariant[2].reference[0].value "variants2"
+queryprofile[1].id "variants2"
+queryprofile[1].type ""
+queryprofile[1].dimensions[0] "x"
+queryprofile[1].property[0].name "c"
+queryprofile[1].property[0].value "c-df"
+queryprofile[1].property[0].overridable ""
+queryprofile[1].queryprofilevariant[0].fordimensionvalues[0] "x1"
+queryprofile[1].queryprofilevariant[0].property[0].name "c"
+queryprofile[1].queryprofilevariant[0].property[0].value "x1.c"
+queryprofile[1].queryprofilevariant[1].fordimensionvalues[0] "x2"
+queryprofile[1].queryprofilevariant[1].property[0].name "c"
+queryprofile[1].queryprofilevariant[1].property[0].value "x2.c"
+queryprofile[2].id "wparent2"
+queryprofile[2].type ""
+queryprofile[2].property[0].name "a"
+queryprofile[2].property[0].value "a1"
+queryprofile[2].property[0].overridable "" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants2-configuration.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants2-configuration.cfg
new file mode 100644
index 00000000000..c915cd2efd0
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profile-variants2-configuration.cfg
@@ -0,0 +1,61 @@
+queryprofile[0].id "default"
+queryprofile[0].type ""
+queryprofile[0].property[0].name "hits"
+queryprofile[0].property[0].value "5"
+queryprofile[0].property[0].overridable ""
+queryprofile[0].property[1].name "model.defaultIndex"
+queryprofile[0].property[1].value "title"
+queryprofile[0].property[1].overridable ""
+queryprofile[0].property[2].name "ranking.features.query(scorelimit)"
+queryprofile[0].property[2].value "-20"
+queryprofile[0].property[2].overridable ""
+queryprofile[0].property[3].name "ranking.profile"
+queryprofile[0].property[3].value "production1"
+queryprofile[0].property[3].overridable ""
+queryprofile[0].property[4].name "ranking.properties.dotProduct.X"
+queryprofile[0].property[4].value "(a:1,b:2)"
+queryprofile[0].property[4].overridable ""
+queryprofile[1].id "multi"
+queryprofile[1].type ""
+queryprofile[1].inherit[0] "default"
+queryprofile[1].dimensions[0] "myquery"
+queryprofile[1].dimensions[1] "myindex"
+queryprofile[1].property[0].name "model.defaultIndex"
+queryprofile[1].property[0].value "default-default"
+queryprofile[1].property[0].overridable ""
+queryprofile[1].reference[0].name "model"
+queryprofile[1].reference[0].value "querybest"
+queryprofile[1].reference[0].overridable ""
+queryprofile[1].queryprofilevariant[0].fordimensionvalues[0] "love"
+queryprofile[1].queryprofilevariant[0].fordimensionvalues[1] "default"
+queryprofile[1].queryprofilevariant[0].reference[0].name "model"
+queryprofile[1].queryprofilevariant[0].reference[0].value "querylove"
+queryprofile[1].queryprofilevariant[1].fordimensionvalues[0] "*"
+queryprofile[1].queryprofilevariant[1].fordimensionvalues[1] "default"
+queryprofile[1].queryprofilevariant[1].property[0].name "model.defaultIndex"
+queryprofile[1].queryprofilevariant[1].property[0].value "default"
+queryprofile[1].queryprofilevariant[2].fordimensionvalues[0] "love"
+queryprofile[1].queryprofilevariant[2].reference[0].name "model"
+queryprofile[1].queryprofilevariant[2].reference[0].value "querylove"
+queryprofile[2].id "querybest"
+queryprofile[2].type "model"
+queryprofile[2].property[0].name "defaultIndex"
+queryprofile[2].property[0].value "title"
+queryprofile[2].property[0].overridable ""
+queryprofile[2].property[1].name "queryString"
+queryprofile[2].property[1].value "best"
+queryprofile[2].property[1].overridable "false"
+queryprofile[3].id "querylove"
+queryprofile[3].type "model"
+queryprofile[3].dimensions[0] "myquery"
+queryprofile[3].dimensions[1] "myindex"
+queryprofile[3].property[0].name "defaultIndex"
+queryprofile[3].property[0].value "title"
+queryprofile[3].property[0].overridable ""
+queryprofile[3].property[1].name "queryString"
+queryprofile[3].property[1].value "love"
+queryprofile[3].property[1].overridable "false"
+queryprofile[3].queryprofilevariant[0].fordimensionvalues[0] "love"
+queryprofile[3].queryprofilevariant[0].fordimensionvalues[1] "default"
+queryprofile[3].queryprofilevariant[0].property[0].name "defaultIndex"
+queryprofile[3].queryprofilevariant[0].property[0].value "default" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profiles.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profiles.cfg
new file mode 100644
index 00000000000..89a971adb15
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/query-profiles.cfg
@@ -0,0 +1,105 @@
+queryprofile[0].id "default"
+queryprofile[0].type ""
+queryprofile[0].property[0].name "ranking"
+queryprofile[0].property[0].value "production23"
+queryprofile[0].property[0].overridable ""
+queryprofile[0].property[1].name "representation.defaultIndex"
+queryprofile[0].property[1].value "title"
+queryprofile[0].property[1].overridable "false"
+queryprofile[1].id "test"
+queryprofile[1].type ""
+queryprofile[1].property[0].name "tracelevel"
+queryprofile[1].property[0].value "2"
+queryprofile[1].property[0].overridable ""
+queryprofile[2].id "genericUser"
+queryprofile[2].type "user"
+queryprofile[2].property[0].name "ads"
+queryprofile[2].property[0].value "all"
+queryprofile[2].property[0].overridable ""
+queryprofile[2].property[1].name "robot"
+queryprofile[2].property[1].value "false"
+queryprofile[2].property[1].overridable ""
+queryprofile[3].id "root"
+queryprofile[3].type "root"
+queryprofile[3].inherit[0] "default"
+queryprofile[3].inherit[1] "test"
+queryprofile[3].property[0].name "defaultage"
+queryprofile[3].property[0].value "7d"
+queryprofile[3].property[0].overridable ""
+queryprofile[3].property[1].name "hits"
+queryprofile[3].property[1].value "30"
+queryprofile[3].property[1].overridable "false"
+queryprofile[3].property[2].name "unique"
+queryprofile[3].property[2].value "category"
+queryprofile[3].property[2].overridable ""
+queryprofile[3].reference[0].name "user"
+queryprofile[3].reference[0].value "genericUser"
+queryprofile[3].reference[0].overridable ""
+queryprofile[4].id "marketUser"
+queryprofile[4].type "user"
+queryprofile[4].inherit[0] "genericUser"
+queryprofile[4].property[0].name "ads"
+queryprofile[4].property[0].value "none"
+queryprofile[4].property[0].overridable ""
+queryprofile[4].property[1].name "age"
+queryprofile[4].property[1].value "25"
+queryprofile[4].property[1].overridable ""
+queryprofile[5].id "root/market"
+queryprofile[5].type "market"
+queryprofile[5].inherit[0] "root"
+queryprofile[5].property[0].name "hits"
+queryprofile[5].property[0].value "15"
+queryprofile[5].property[0].overridable ""
+queryprofile[5].property[1].name "market"
+queryprofile[5].property[1].value "some market"
+queryprofile[5].property[1].overridable ""
+queryprofile[5].property[2].name "marketHeading"
+queryprofile[5].property[2].value "Market of %{market}"
+queryprofile[5].property[2].overridable ""
+queryprofile[5].reference[0].name "user"
+queryprofile[5].reference[0].value "marketUser"
+queryprofile[5].reference[0].overridable ""
+queryprofile[6].id "untypedUser"
+queryprofile[6].type ""
+queryprofile[6].property[0].name "robot"
+queryprofile[6].property[0].value "false"
+queryprofile[6].property[0].overridable ""
+queryprofile[6].property[1].name "robot.type"
+queryprofile[6].property[1].value "continent-class"
+queryprofile[6].property[1].overridable ""
+queryprofiletype[0].id "user"
+queryprofiletype[0].strict true
+queryprofiletype[0].matchaspath false
+queryprofiletype[0].field[0].name "ads"
+queryprofiletype[0].field[0].type "string"
+queryprofiletype[0].field[0].overridable true
+queryprofiletype[0].field[0].mandatory true
+queryprofiletype[0].field[0].alias ""
+queryprofiletype[0].field[1].name "age"
+queryprofiletype[0].field[1].type "integer"
+queryprofiletype[0].field[1].overridable true
+queryprofiletype[0].field[1].mandatory false
+queryprofiletype[0].field[1].alias ""
+queryprofiletype[0].field[2].name "robot"
+queryprofiletype[0].field[2].type "boolean"
+queryprofiletype[0].field[2].overridable false
+queryprofiletype[0].field[2].mandatory true
+queryprofiletype[0].field[2].alias "machine automaton"
+queryprofiletype[1].id "root"
+queryprofiletype[1].strict false
+queryprofiletype[1].matchaspath true
+queryprofiletype[1].inherit[0] "native"
+queryprofiletype[1].field[0].name "user"
+queryprofiletype[1].field[0].type "query-profile:user"
+queryprofiletype[1].field[0].overridable true
+queryprofiletype[1].field[0].mandatory true
+queryprofiletype[1].field[0].alias ""
+queryprofiletype[2].id "market"
+queryprofiletype[2].strict false
+queryprofiletype[2].matchaspath false
+queryprofiletype[2].inherit[0] "root"
+queryprofiletype[2].field[0].name "market"
+queryprofiletype[2].field[0].type "string"
+queryprofiletype[2].field[0].overridable false
+queryprofiletype[2].field[0].mandatory false
+queryprofiletype[2].field[0].alias "" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants1.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants1.xml
new file mode 100644
index 00000000000..b4a1398a83e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants1.xml
@@ -0,0 +1,16 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="variants1">
+ <dimensions>x,y</dimensions>
+ <field name="a">a-deflt</field>
+ <query-profile for="x1,y1" inherits="variants2 wparent2">
+ <field name="a">x1.y1.a</field>
+ <field name="b">x1.y1.b</field>
+ </query-profile>
+ <query-profile for="x1">
+ <field name="a">x1.y?.a</field>
+ </query-profile>
+ <query-profile for="*,y1">
+ <field name="a">x?.y1.a</field>
+ <field name="toVariants2"><ref>variants2</ref></field>
+ </query-profile>
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants2.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants2.xml
new file mode 100644
index 00000000000..23ce86b07a6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/variants2.xml
@@ -0,0 +1,11 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="variants2">
+ <dimensions>x</dimensions>
+ <field name="c">c-df</field>
+ <query-profile for="x1">
+ <field name="c">x1.c</field>
+ </query-profile>
+ <query-profile for="x2">
+ <field name="c">x2.c</field>
+ </query-profile>
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/wparent2.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/wparent2.xml
new file mode 100644
index 00000000000..84ae3bd8c01
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants/wparent2.xml
@@ -0,0 +1,4 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="wparent2">
+ <field name="a">a1</field>
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/default.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/default.xml
new file mode 100644
index 00000000000..4ffe97ef9a2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/default.xml
@@ -0,0 +1,8 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="default">
+ <field name="hits">5</field>
+ <field name="model.defaultIndex">title</field>
+ <field name="ranking.profile">production1</field>
+ <field name="ranking.features.query(scorelimit)">-20</field>
+ <field name="ranking.properties.dotProduct.X">(a:1,b:2)</field>
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/multi.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/multi.xml
new file mode 100644
index 00000000000..0bd52c8f8ee
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/multi.xml
@@ -0,0 +1,20 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="multi" inherits="default"> <!-- default sets default-index to title -->
+ <dimensions>myquery, myindex </dimensions>
+ <field name="model"><ref>querybest</ref></field>
+ <field name="model.defaultIndex">default-default</field>
+
+ <query-profile for="love,default">
+ <field name="model"><ref>querylove</ref></field>
+ <field name="model.defaultIndex">default</field>
+ </query-profile>
+
+ <query-profile for="*,default">
+ <field name="model.defaultIndex">default</field>
+ </query-profile>
+
+ <query-profile for="love">
+ <field name="model"><ref>querylove</ref></field>
+ </query-profile>
+
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querybest.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querybest.xml
new file mode 100644
index 00000000000..9a957012de4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querybest.xml
@@ -0,0 +1,6 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="querybest" type="model">
+ <field name="defaultIndex">title</field>
+ <field name="queryString" overridable="false">best</field>
+</query-profile>
+
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querylove.xml b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querylove.xml
new file mode 100644
index 00000000000..e7864977804
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/queryprofilevariants2/querylove.xml
@@ -0,0 +1,5 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<query-profile id="querylove" type="model">
+ <field name="defaultIndex">title</field>
+ <field name="queryString" overridable="false">love</field>
+</query-profile>
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound-with-reference.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound-with-reference.cfg
new file mode 100644
index 00000000000..e1cca7ed232
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound-with-reference.cfg
@@ -0,0 +1,26 @@
+queryprofile[0].id "a1"
+queryprofile[0].type ""
+queryprofile[0].property[0].name "b"
+queryprofile[0].property[0].value "a1.b"
+queryprofile[0].property[0].overridable ""
+queryprofile[1].id "a2"
+queryprofile[1].type ""
+queryprofile[1].dimensions[0] "x"
+queryprofile[1].property[0].name "b"
+queryprofile[1].property[0].value "a2.b"
+queryprofile[1].property[0].overridable ""
+queryprofile[1].queryprofilevariant[0].fordimensionvalues[0] "x1"
+queryprofile[1].queryprofilevariant[0].property[0].name "b"
+queryprofile[1].queryprofilevariant[0].property[0].value "a.b.x1"
+queryprofile[2].id "test"
+queryprofile[2].type ""
+queryprofile[2].dimensions[0] "x"
+queryprofile[2].reference[0].name "a"
+queryprofile[2].reference[0].value "a1"
+queryprofile[2].reference[0].overridable ""
+queryprofile[2].queryprofilevariant[0].fordimensionvalues[0] "x1"
+queryprofile[2].queryprofilevariant[0].reference[0].name "a"
+queryprofile[2].queryprofilevariant[0].reference[0].value "a2"
+queryprofile[2].queryprofilevariant[1].fordimensionvalues[0] "x2"
+queryprofile[2].queryprofilevariant[1].property[0].name "a.b"
+queryprofile[2].queryprofilevariant[1].property[0].value "a.b.x2" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound.cfg b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound.cfg
new file mode 100644
index 00000000000..d65b3fa5f92
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/search/test/variants-of-explicit-compound.cfg
@@ -0,0 +1,17 @@
+queryprofile[0].id "a1"
+queryprofile[0].type ""
+queryprofile[0].property[0].name "b"
+queryprofile[0].property[0].value "a1.b"
+queryprofile[0].property[0].overridable ""
+queryprofile[1].id "test"
+queryprofile[1].type ""
+queryprofile[1].dimensions[0] "x"
+queryprofile[1].reference[0].name "a"
+queryprofile[1].reference[0].value "a1"
+queryprofile[1].reference[0].overridable ""
+queryprofile[1].queryprofilevariant[0].fordimensionvalues[0] "x1"
+queryprofile[1].queryprofilevariant[0].property[0].name "a.b"
+queryprofile[1].queryprofilevariant[0].property[0].value "a.b.x1"
+queryprofile[1].queryprofilevariant[1].fordimensionvalues[0] "x2"
+queryprofile[1].queryprofilevariant[1].property[0].name "a.b"
+queryprofile[1].queryprofilevariant[1].property[0].value "a.b.x2" \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java
new file mode 100644
index 00000000000..99f4387c140
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/AccessLogTest.java
@@ -0,0 +1,90 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.core.AccessLogConfig;
+import com.yahoo.container.logging.VespaAccessLog;
+import com.yahoo.container.logging.YApacheAccessLog;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import static com.yahoo.text.StringUtilities.quote;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ * @since 5.5
+ */
+public class AccessLogTest extends ContainerModelBuilderTestBase {
+
+ @Test
+ public void default_access_log_is_only_added_when_search_is_present() throws Exception {
+ Element cluster1Elem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0'>",
+ "<search />",
+ nodesXml,
+ "</jdisc>");
+ Element cluster2Elem = DomBuilderTest.parse(
+ "<jdisc id='cluster2' version='1.0'>",
+ " <nodes>",
+ " <node hostalias='mockhost' baseport='1234' />",
+ " </nodes>",
+ "</jdisc>" );
+
+ createModel(root, cluster1Elem, cluster2Elem);
+
+ assertNotNull(getVespaAccessLog("cluster1"));
+ assertNull( getVespaAccessLog("cluster2"));
+ }
+
+ @Test
+ public void default_search_access_log_can_be_disabled() throws Exception {
+ final String jdiscClusterId = "jdisc-cluster";
+
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id=" + quote(jdiscClusterId) + " version='1.0'>" +
+ " <search />" +
+ " <accesslog type='disabled' />" +
+ "</jdisc>" );
+
+ createModel(root, clusterElem);
+ assertNull(getVespaAccessLog(jdiscClusterId));
+ }
+
+ private Component<?, ?> getVespaAccessLog(String clusterName) {
+ ContainerCluster cluster = (ContainerCluster) root.getChildren().get(clusterName);
+ return cluster.getComponentsMap().get(ComponentId.fromString((VespaAccessLog.class.getName())));
+
+ }
+
+ @Test
+ public void access_log_can_be_configured() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <accesslog type='yapache' ",
+ " fileNamePattern='pattern' rotationInterval='interval'",
+ " rotationScheme='date' />",
+ nodesXml,
+ "</jdisc>" );
+
+ createModel(root, clusterElem);
+
+ Component<?, ?> accessLogComponent = getContainerComponent("default", YApacheAccessLog.class.getName());
+ assertNotNull(accessLogComponent);
+ assertThat(accessLogComponent.getClassId().getName(), is(YApacheAccessLog.class.getName()));
+
+ AccessLogConfig config = root.getConfig(AccessLogConfig.class, "default/component/com.yahoo.container.logging.YApacheAccessLog");
+ AccessLogConfig.FileHandler fileHandlerConfig = config.fileHandler();
+ assertThat(fileHandlerConfig.pattern(), is("pattern"));
+ assertThat(fileHandlerConfig.rotation(), is("interval"));
+ assertThat(fileHandlerConfig.rotateScheme(), is(AccessLogConfig.FileHandler.RotateScheme.DATE));
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilderTest.java
new file mode 100644
index 00000000000..7a8a554e650
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ConfigServerContainerModelBuilderTest.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.cloud.config.ElkConfig;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.container.configserver.TestOptions;
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.17
+ */
+public class ConfigServerContainerModelBuilderTest {
+ @Test
+ public void testHostedVespaInclude() {
+ File testApp = new File("src/test/cfg/container/data/configserverinclude");
+ FilesApplicationPackage app = FilesApplicationPackage.fromFile(testApp);
+ MockRoot root = new MockRoot();
+ new ConfigServerContainerModelBuilder(new TestOptions()).build(new DeployState.Builder().applicationPackage(app).build(), null, root, XML.getChild(XML.getDocument(app.getServices()).getDocumentElement(), "jdisc"));
+ root.freezeModelTopology();
+ ElkConfig config = root.getConfig(ElkConfig.class, "configserver/configserver");
+ assertThat(config.elasticsearch().size(), is(1));
+ assertThat(config.elasticsearch(0).host(), is("foo"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerDocumentApiBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerDocumentApiBuilderTest.java
new file mode 100644
index 00000000000..110b4065c8f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerDocumentApiBuilderTest.java
@@ -0,0 +1,173 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Handler;
+import com.yahoo.vespaclient.config.FeederConfig;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.hasItem;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.11
+ */
+public class ContainerDocumentApiBuilderTest extends ContainerModelBuilderTestBase {
+
+ private Map<String, Handler<?>> getHandlers(String clusterName) {
+ ContainerCluster cluster = (ContainerCluster) root.getChildren().get(clusterName);
+ Map<String, Handler<?>> handlerMap = new HashMap<>();
+ Collection<Handler<?>> handlers = cluster.getHandlers();
+ for (Handler<?> handler : handlers) {
+ assertThat(handlerMap.containsKey(handler.getComponentId().toString()), is(false)); //die on overwrites
+ handlerMap.put(handler.getComponentId().toString(), handler);
+ }
+ return handlerMap;
+ }
+
+ @Test
+ public void document_api_config_is_added_to_container_cluster() throws Exception {
+ Element elem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0'>",
+ " <document-api>",
+ " <abortondocumenterror>false</abortondocumenterror>",
+ " <maxpendingdocs>4321</maxpendingdocs>",
+ " <retrydelay>12.34</retrydelay>",
+ " <route>non-default</route>",
+ " </document-api>",
+ nodesXml,
+ "</jdisc>");
+ createModel(root, elem);
+ ContainerCluster cluster = (ContainerCluster)root.getProducer("cluster1");
+ FeederConfig.Builder builder = new FeederConfig.Builder();
+ cluster.getDocumentApi().getConfig(builder);
+ FeederConfig config = new FeederConfig(builder);
+ assertThat(config.abortondocumenterror(), is(false));
+ assertThat(config.maxpendingdocs(), is(4321));
+ assertThat(config.retrydelay(), is(12.34));
+ assertThat(config.route(), is("non-default"));
+ }
+
+ @Test
+ public void custom_bindings_are_allowed() throws Exception {
+ Element elem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0'>",
+ " <document-api>",
+ " <binding>https://*/document-api/</binding>",
+ " <binding>missing-trailing-slash</binding>",
+ " </document-api>",
+ nodesXml,
+ "</jdisc>");
+ createModel(root, elem);
+
+ verifyCustomBindings("com.yahoo.feedhandler.VespaFeedHandler", "feed");
+ verifyCustomBindings("com.yahoo.feedhandler.VespaFeedHandlerCompatibility", "document");
+ verifyCustomBindings("com.yahoo.feedhandler.VespaFeedHandlerGet", "get");
+ verifyCustomBindings("com.yahoo.feedhandler.VespaFeedHandlerRemove", "remove");
+ verifyCustomBindings("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation", "removelocation");
+ verifyCustomBindings("com.yahoo.feedhandler.VespaFeedHandlerStatus", "feedstatus");
+ verifyCustomBindings("com.yahoo.feedhandler.VespaFeedHandlerVisit", "visit");
+ verifyCustomBindings("com.yahoo.vespa.http.server.FeedHandler", ContainerCluster.RESERVED_URI_PREFIX + "/feedapi");
+ }
+
+ private void verifyCustomBindings(String id, String bindingSuffix) {
+ Handler<?> handler = getHandlers("cluster1").get(id);
+
+ assertThat(handler.getServerBindings(), hasItem("https://*/document-api/" + bindingSuffix));
+ assertThat(handler.getServerBindings(), hasItem("https://*/document-api/" + bindingSuffix + "/"));
+ assertThat(handler.getServerBindings(), hasItem("missing-trailing-slash/" + bindingSuffix));
+ assertThat(handler.getServerBindings(), hasItem("missing-trailing-slash/" + bindingSuffix + "/"));
+
+ assertThat(handler.getServerBindings().size(), is(4));
+ }
+
+ @Test
+ public void requireThatHandlersAreSetup() throws Exception {
+ Element elem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0'>",
+ " <document-api />",
+ nodesXml,
+ "</jdisc>");
+ createModel(root, elem);
+
+ Map<String, Handler<?>> handlerMap = getHandlers("cluster1");
+
+ assertThat(handlerMap.get("com.yahoo.container.config.StatisticsRequestHandler"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.container.handler.VipStatusHandler"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.container.handler.observability.ApplicationStatusHandler"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.container.jdisc.state.StateHandler"), not(nullValue()));
+
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandler"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandler").getServerBindings().contains("http://*/feed"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandler").getServerBindings().contains("https://*/feed"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandler").getServerBindings().contains("http://*/feed/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandler").getServerBindings().contains("https://*/feed/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandler").getServerBindings().size(), equalTo(4));
+
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerCompatibility").getComponentId().toString(), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerCompatibility").getServerBindings().contains("http://*/document"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerCompatibility").getServerBindings().contains("https://*/document"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerCompatibility").getServerBindings().contains("http://*/document/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerCompatibility").getServerBindings().contains("https://*/document/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerCompatibility").getServerBindings().size(), equalTo(4));
+
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerGet"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerGet").getServerBindings().contains("http://*/get"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerGet").getServerBindings().contains("https://*/get"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerGet").getServerBindings().contains("http://*/get/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerGet").getServerBindings().contains("https://*/get/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerGet").getServerBindings().size(), equalTo(4));
+
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemove"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemove").getServerBindings().contains("http://*/remove"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemove").getServerBindings().contains("https://*/remove"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemove").getServerBindings().contains("http://*/remove/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemove").getServerBindings().contains("https://*/remove/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemove").getServerBindings().size(), equalTo(4));
+
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation").getServerBindings().contains("http://*/removelocation"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation").getServerBindings().contains("https://*/removelocation"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation").getServerBindings().contains("http://*/removelocation/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation").getServerBindings().contains("https://*/removelocation/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerRemoveLocation").getServerBindings().size(), equalTo(4));
+
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerStatus"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerStatus").getServerBindings().contains("http://*/feedstatus"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerStatus").getServerBindings().contains("https://*/feedstatus"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerStatus").getServerBindings().contains("http://*/feedstatus/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerStatus").getServerBindings().contains("https://*/feedstatus/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerStatus").getServerBindings().size(), equalTo(4));
+
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerVisit"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerVisit").getServerBindings().contains("http://*/visit"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerVisit").getServerBindings().contains("https://*/visit"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerVisit").getServerBindings().contains("http://*/visit/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerVisit").getServerBindings().contains("https://*/visit/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.feedhandler.VespaFeedHandlerVisit").getServerBindings().size(), equalTo(4));
+
+ assertThat(handlerMap.get("com.yahoo.search.handler.SearchHandler"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.search.handler.SearchHandler").getServerBindings().contains("http://*/search/*"), is(true));
+ assertThat(handlerMap.get("com.yahoo.search.handler.SearchHandler").getServerBindings().contains("https://*/search/*"), is(true));
+ assertThat(handlerMap.get("com.yahoo.search.handler.SearchHandler").getServerBindings().size(), equalTo(2));
+
+ assertThat(handlerMap.get("com.yahoo.vespa.http.server.FeedHandler"), not(nullValue()));
+ assertThat(handlerMap.get("com.yahoo.vespa.http.server.FeedHandler").getServerBindings().contains("http://*/" + ContainerCluster.RESERVED_URI_PREFIX + "/feedapi"), is(true));
+ assertThat(handlerMap.get("com.yahoo.vespa.http.server.FeedHandler").getServerBindings().contains("https://*/" + ContainerCluster.RESERVED_URI_PREFIX + "/feedapi"), is(true));
+ assertThat(handlerMap.get("com.yahoo.vespa.http.server.FeedHandler").getServerBindings().contains("http://*/" + ContainerCluster.RESERVED_URI_PREFIX + "/feedapi/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.vespa.http.server.FeedHandler").getServerBindings().contains("https://*/" + ContainerCluster.RESERVED_URI_PREFIX + "/feedapi/"), is(true));
+ assertThat(handlerMap.get("com.yahoo.vespa.http.server.FeedHandler").getServerBindings().size(), equalTo(4));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java
new file mode 100644
index 00000000000..3ea0a9732d6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java
@@ -0,0 +1,573 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.producer.AbstractConfigProducerRoot;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.container.config.StatisticsRequestHandler;
+import com.yahoo.container.core.ChainsConfig;
+import com.yahoo.container.core.VipStatusConfig;
+import com.yahoo.container.servlet.ServletConfigConfig;
+import com.yahoo.container.handler.VipStatusHandler;
+import com.yahoo.container.handler.observability.ApplicationStatusHandler;
+import com.yahoo.container.jdisc.JdiscBindingsConfig;
+import com.yahoo.container.usability.BindingsOverviewHandler;
+import com.yahoo.jdisc.http.ServletPathsConfig;
+import com.yahoo.prelude.cluster.QrMonitorConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.component.HttpFilter;
+import com.yahoo.vespa.model.content.utils.ContentClusterUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+
+import static com.yahoo.test.LinePatternMatcher.containsLineWithPattern;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasItem;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author gjoranv
+ * @since 5.1.9
+ */
+public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase {
+
+ @Test
+ public void default_port_is_4080() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc version='1.0'>",
+ nodesXml,
+ "</jdisc>" );
+ createModel(root, clusterElem);
+ AbstractService container = (AbstractService)root.getProducer("jdisc/container.0");
+ assertThat(container.getRelativePort(0), is(Defaults.getDefaults().vespaWebServicePort()));
+ }
+
+ @Test
+ public void http_server_port_is_configurable_and_does_not_affect_other_ports() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc version='1.0'>",
+ " <http>",
+ " <server port='9000' id='foo' />",
+ " </http>",
+ nodesXml,
+ "</jdisc>" );
+ createModel(root, clusterElem);
+ AbstractService container = (AbstractService)root.getProducer("jdisc/container.0");
+ assertThat(container.getRelativePort(0), is(9000));
+ assertThat(container.getRelativePort(1), is(not(9001)));
+ }
+
+ @Test
+ public void fail_if_http_port_is_not_4080_in_hosted_vespa() throws Exception {
+ String servicesXml =
+ "<services>" +
+ "<admin version='3.0'>" +
+ " <nodes count='1'/>" +
+ "</admin>" +
+ "<jdisc version='1.0'>" +
+ " <http>" +
+ " <server port='9000' id='foo' />" +
+ " </http>" +
+ nodesXml +
+ "</jdisc>" +
+ "</services>";
+ ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build();
+ // Need to create VespaModel to make deploy properties have effect
+ final MyLogger logger = new MyLogger();
+ new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder()
+ .applicationPackage(applicationPackage)
+ .deployLogger(logger)
+ .properties(new DeployProperties.Builder()
+ .hostedVespa(true)
+ .build())
+ .build());
+ assertFalse(logger.msgs.isEmpty());
+ assertThat(logger.msgs.get(0).getSecond(), containsString(String.format("You cannot set port to anything else than %d", Container.BASEPORT)));
+ }
+
+ private class MyLogger implements DeployLogger {
+ List<Pair<Level, String>> msgs = new ArrayList<>();
+ @Override
+ public void log(Level level, String message) {
+ msgs.add(new Pair<>(level, message));
+ }
+ }
+
+ @Test
+ public void one_cluster_with_explicit_port_and_one_without_is_ok() throws Exception {
+ Element cluster1Elem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0' />");
+ Element cluster2Elem = DomBuilderTest.parse(
+ "<jdisc id='cluster2' version='1.0'>",
+ " <http>",
+ " <server port='8000' id='foo' />",
+ " </http>",
+ "</jdisc>");
+ createModel(root, cluster1Elem, cluster2Elem);
+ }
+
+ @Test
+ public void two_clusters_without_explicit_port_throws_exception() throws SAXException, IOException {
+ Element cluster1Elem = DomBuilderTest.parse(
+ "<jdisc id='cluster1' version='1.0'>",
+ nodesXml,
+ "</jdisc>" );
+ Element cluster2Elem = DomBuilderTest.parse(
+ "<jdisc id='cluster2' version='1.0'>",
+ nodesXml,
+ "</jdisc>" );
+ try {
+ createModel(root, cluster1Elem, cluster2Elem);
+ fail("Expected exception");
+ } catch (RuntimeException e) {
+ assertThat(e.getMessage(), containsString("cannot reserve port"));
+ }
+ }
+
+ @Test
+ public void verify_bindings_for_builtin_handlers() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0' />"
+ );
+ createModel(root, clusterElem);
+ JdiscBindingsConfig config = root.getConfig(JdiscBindingsConfig.class, "default/container.0");
+
+ JdiscBindingsConfig.Handlers defaultRootHandler = config.handlers(BindingsOverviewHandler.class.getName());
+ assertThat(defaultRootHandler.serverBindings(), contains("*://*/"));
+
+ JdiscBindingsConfig.Handlers applicationStatusHandler = config.handlers(ApplicationStatusHandler.class.getName());
+ assertThat(applicationStatusHandler.serverBindings(),
+ contains("http://*/ApplicationStatus", "https://*/ApplicationStatus"));
+
+ JdiscBindingsConfig.Handlers statisticsRequestHandler = config.handlers(StatisticsRequestHandler.class.getName());
+ assertTrue(statisticsRequestHandler.serverBindings(0).startsWith("http://*/statistics"));
+ assertTrue(statisticsRequestHandler.serverBindings(1).startsWith("https://*/statistics"));
+
+ JdiscBindingsConfig.Handlers fileRequestHandler = config.handlers(VipStatusHandler.class.getName());
+ assertThat(fileRequestHandler.serverBindings(),
+ contains("http://*/status.html", "https://*/status.html"));
+ }
+
+ @Test
+ public void default_root_handler_is_disabled_when_user_adds_a_handler_with_same_binding() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>" +
+ " <handler id='userRootHandler'>" +
+ " <binding>" + ContainerCluster.ROOT_HANDLER_BINDING + "</binding>" +
+ " </handler>" +
+ "</jdisc>");
+ createModel(root, clusterElem);
+
+ ComponentsConfig.Components userRootHandler = getComponent(componentsConfig(), BindingsOverviewHandler.class.getName());
+ assertThat(userRootHandler, nullValue());
+ }
+
+ @Test
+ public void handler_bindings_are_included_in_discBindings_config() throws Exception {
+ createClusterWithJDiscHandler();
+ String discBindingsConfig = root.getConfig(JdiscBindingsConfig.class, "default").toString();
+ assertThat(discBindingsConfig, containsString("{discHandler}"));
+ assertThat(discBindingsConfig, containsString(".serverBindings[0] \"binding0\""));
+ assertThat(discBindingsConfig, containsString(".serverBindings[1] \"binding1\""));
+ assertThat(discBindingsConfig, containsString(".clientBindings[0] \"clientBinding\""));
+ }
+
+ @Test
+ public void handlers_are_included_in_components_config() throws Exception {
+ createClusterWithJDiscHandler();
+ assertThat(componentsConfig().toString(), containsString(".id \"discHandler\""));
+ }
+
+ private void createClusterWithJDiscHandler() throws SAXException, IOException {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <handler id='discHandler'>",
+ " <binding>binding0</binding>",
+ " <binding>binding1</binding>",
+ " <clientBinding>clientBinding</clientBinding>",
+ " </handler>",
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+ }
+
+ @Test
+ public void servlets_are_included_in_ServletPathConfig() throws Exception {
+ createClusterWithServlet();
+ ServletPathsConfig servletPathsConfig = root.getConfig(ServletPathsConfig.class, "default");
+ assertThat(servletPathsConfig.servlets().values().iterator().next().path(), is("p/a/t/h"));
+ }
+
+ @Test
+ public void servletconfig_is_produced() throws Exception {
+ createClusterWithServlet();
+
+ String configId = getContainerCluster("default").getServletMap().
+ values().iterator().next().getConfigId();
+
+ ServletConfigConfig servletConfig = root.getConfig(ServletConfigConfig.class, configId);
+
+ assertThat(servletConfig.map().get("myKey"), is("myValue"));
+ }
+
+ private void createClusterWithServlet() throws SAXException, IOException {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <servlet id='myServlet' class='myClass' bundle='myBundle'>",
+ " <path>p/a/t/h</path>",
+ " <servlet-config>",
+ " <myKey>myValue</myKey>",
+ " </servlet-config>",
+ " </servlet>",
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+ }
+
+
+ @Test
+ public void processing_handler_bindings_can_be_overridden() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <processing>",
+ " <binding>binding0</binding>",
+ " <binding>binding1</binding>",
+ " </processing>",
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+
+ String discBindingsConfig = root.getConfig(JdiscBindingsConfig.class, "default").toString();
+ assertThat(discBindingsConfig, containsString(".serverBindings[0] \"binding0\""));
+ assertThat(discBindingsConfig, containsString(".serverBindings[1] \"binding1\""));
+ assertThat(discBindingsConfig, not(containsString("/processing/*")));
+ }
+
+ @Test
+ public void clientProvider_bindings_are_included_in_discBindings_config() throws Exception {
+ createModelWithClientProvider();
+ String discBindingsConfig = root.getConfig(JdiscBindingsConfig.class, "default").toString();
+ assertThat(discBindingsConfig, containsString("{discClient}"));
+ assertThat(discBindingsConfig, containsString(".clientBindings[0] \"binding0\""));
+ assertThat(discBindingsConfig, containsString(".clientBindings[1] \"binding1\""));
+ assertThat(discBindingsConfig, containsString(".serverBindings[0] \"serverBinding\""));
+ }
+
+ @Test
+ public void clientProviders_are_included_in_components_config() throws Exception {
+ createModelWithClientProvider();
+ assertThat(componentsConfig().toString(), containsString(".id \"discClient\""));
+ }
+
+ private void createModelWithClientProvider() throws SAXException, IOException {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>" +
+ " <client id='discClient'>" +
+ " <binding>binding0</binding>" +
+ " <binding>binding1</binding>" +
+ " <serverBinding>serverBinding</serverBinding>" +
+ " </client>" +
+ "</jdisc>" );
+
+ createModel(root, clusterElem);
+ }
+
+ @Test
+ public void serverProviders_are_included_in_components_config() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>" +
+ " <server id='discServer' />" +
+ "</jdisc>" );
+
+ createModel(root, clusterElem);
+
+ String componentsConfig = componentsConfig().toString();
+ assertThat(componentsConfig, containsString(".id \"discServer\""));
+ }
+
+ private String getChainsConfig(String configId) {
+ return root.getConfig(ChainsConfig.class, configId).toString();
+ }
+
+ @Test
+ public void searchHandler_gets_only_search_chains_in_chains_config() throws Exception {
+ createClusterWithProcessingAndSearchChains();
+ String searchHandlerConfigId = "default/component/com.yahoo.search.handler.SearchHandler";
+ String chainsConfig = getChainsConfig(searchHandlerConfigId);
+ assertThat(chainsConfig, containsLineWithPattern(".*\\.id \"testSearcher@default\"$"));
+ assertThat(chainsConfig, not(containsLineWithPattern(".*\\.id \"testProcessor@default\"$")));
+ }
+
+ @Test
+ public void processingHandler_gets_only_processing_chains_in_chains_config() throws Exception {
+ createClusterWithProcessingAndSearchChains();
+ String processingHandlerConfigId = "default/component/com.yahoo.processing.handler.ProcessingHandler";
+ String chainsConfig = getChainsConfig(processingHandlerConfigId);
+ assertThat(chainsConfig, containsLineWithPattern(".*\\.id \"testProcessor@default\"$"));
+ assertThat(chainsConfig, not(containsLineWithPattern(".*\\.id \"testSearcher@default\"$")));
+ }
+
+ private void createClusterWithProcessingAndSearchChains() throws SAXException, IOException {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>" +
+ " <search>" +
+ " <chain id='default'>" +
+ " <searcher id='testSearcher' />" +
+ " </chain>" +
+ " </search>" +
+ " <processing>" +
+ " <chain id='default'>" +
+ " <processor id='testProcessor'/>" +
+ " </chain>" +
+ " </processing>" +
+ nodesXml +
+ " </jdisc>");
+
+ createModel(root, clusterElem);
+ }
+
+ @Test
+ public void user_config_can_be_overridden_on_node() throws Exception {
+ Element containerElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <config name=\"prelude.cluster.qr-monitor\">" +
+ " <requesttimeout>111</requesttimeout>",
+ " </config> " +
+ " <nodes>",
+ " <node hostalias='host1' />",
+ " <node hostalias='host2'>",
+ " <config name=\"prelude.cluster.qr-monitor\">",
+ " <requesttimeout>222</requesttimeout>",
+ " </config> ",
+ " </node>",
+ " </nodes>",
+ "</jdisc>");
+
+ root = ContentClusterUtils.createMockRoot(new String[]{"host1", "host2"});
+ createModel(root, containerElem);
+ ContainerCluster cluster = (ContainerCluster)root.getChildren().get("default");
+ assertThat(cluster.getContainers().size(), is(2));
+ assertEquals(root.getConfig(QrMonitorConfig.class, "default/container.0").requesttimeout(), 111);
+ assertEquals(root.getConfig(QrMonitorConfig.class, "default/container.1").requesttimeout(), 222);
+ }
+
+ @Test
+ public void http_section_can_be_set_up() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <http>",
+ " <chain id='filterChain2'>",
+ " <filter id='filter' />",
+ " </chain>",
+ " </http>",
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+
+ root.getChildren();
+ }
+
+ @Test
+ public void legacy_yca_filter_and_its_config_provider_are_included_in_components_config() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <filter id='YcaFilter' /> ",
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+ assertThat(componentsConfig().toString(), containsString(".id \"YcaFilter\""));
+
+ String providerId = HttpFilter.configProviderId(ComponentId.fromString("YcaFilter")).stringValue();
+ assertThat(componentsConfig().toString(), containsString(".id \"" + providerId + "\""));
+ }
+
+ @Test
+ public void nested_components_are_injected_to_handlers() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <handler id='myHandler'>",
+ " <component id='injected' />",
+ " </handler>",
+ " <client id='myClient'>", // remember, a client is also a request handler
+ " <component id='injected' />",
+ " </client>",
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+ Component<?,?> handler = getContainerComponent("default", "myHandler");
+ assertThat(handler.getInjectedComponentIds(), hasItem("injected@myHandler"));
+
+ Component<?,?> client = getContainerComponent("default", "myClient");
+ assertThat(client.getInjectedComponentIds(), hasItem("injected@myClient"));
+ }
+
+ @Test
+ public void component_includes_are_added() {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg("src/test/cfg/application/include_dirs");
+ VespaModel model = creator.create(true);
+ ContainerCluster cluster = model.getContainerClusters().get("default");
+ Map<ComponentId, Component<?, ?>> componentsMap = cluster.getComponentsMap();
+ Component<?,?> example = componentsMap.get(
+ ComponentId.fromString("test.Exampledocproc"));
+ assertThat(example.getComponentId().getName(), is("test.Exampledocproc"));
+ }
+
+ @Test
+ public void affinity_is_set() throws IOException, SAXException {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <http>",
+ " <server port='" + Defaults.getDefaults().vespaWebServicePort() + "' id='main' />",
+ " </http>",
+ " <nodes cpu-socket-affinity='true'>",
+ " <node hostalias='node1' />",
+ " <node hostalias='node2'> <server-port id='main' port='5080'/> </node>",
+ " <node hostalias='node3'> <server-port id='main' port='6080'/> </node>",
+ " <node hostalias='node4'> <server-port id='main' port='7080'/> </node>",
+ " </nodes>" +
+ "</jdisc>");
+ createModel(root, clusterElem);
+ assertTrue(getContainerCluster("default").getContainers().get(0).getAffinity().isPresent());
+ assertTrue(getContainerCluster("default").getContainers().get(1).getAffinity().isPresent());
+ assertTrue(getContainerCluster("default").getContainers().get(2).getAffinity().isPresent());
+ assertTrue(getContainerCluster("default").getContainers().get(3).getAffinity().isPresent());
+
+ assertThat(getContainerCluster("default").getContainers().get(0).getAffinity().get().cpuSocket(), is(0));
+ assertThat(getContainerCluster("default").getContainers().get(1).getAffinity().get().cpuSocket(), is(1));
+ assertThat(getContainerCluster("default").getContainers().get(2).getAffinity().get().cpuSocket(), is(2));
+ assertThat(getContainerCluster("default").getContainers().get(3).getAffinity().get().cpuSocket(), is(3));
+ }
+
+ @Test
+ public void singlenode_servicespec_is_used_with_hosts_xml() throws IOException, SAXException {
+ String servicesXml = "<jdisc id='default' version='1.0' />";
+ String hostsXml = "<hosts>\n" +
+ " <host name=\"test1.yahoo.com\">\n" +
+ " <alias>node1</alias>\n" +
+ " </host>\n" +
+ "</hosts>";
+ ApplicationPackage applicationPackage = new MockApplicationPackage.Builder()
+ .withHosts(hostsXml)
+ .withServices(servicesXml)
+ .build();
+ VespaModel model = new VespaModel(applicationPackage);
+ assertThat(model.getHostSystem().getHosts().size(), is(1));
+ }
+
+ @Test
+ public void http_aliases_are_stored_on_cluster_and_on_service_properties() throws SAXException, IOException {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <aliases>",
+ " <service-alias>service1</service-alias>",
+ " <service-alias>service2</service-alias>",
+ " <endpoint-alias>foo1.bar1.com</endpoint-alias>",
+ " <endpoint-alias>foo2.bar2.com</endpoint-alias>",
+ " </aliases>",
+ " <nodes>",
+ " <node hostalias='host1' />",
+ " </nodes>",
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+ assertEquals(getContainerCluster("default").serviceAliases().get(0), "service1");
+ assertEquals(getContainerCluster("default").endpointAliases().get(0), "foo1.bar1.com");
+ assertEquals(getContainerCluster("default").serviceAliases().get(1), "service2");
+ assertEquals(getContainerCluster("default").endpointAliases().get(1), "foo2.bar2.com");
+
+ assertEquals(getContainerCluster("default").getContainers().get(0).getServicePropertyString("servicealiases"), "service1,service2");
+ assertEquals(getContainerCluster("default").getContainers().get(0).getServicePropertyString("endpointaliases"), "foo1.bar1.com,foo2.bar2.com");
+
+ }
+
+ @Test
+ public void singlenode_servicespec_is_used_with_hosted_vespa() throws IOException, SAXException {
+ String servicesXml = "<jdisc id='default' version='1.0' />";
+ ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build();
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder()
+ .modelHostProvisioner(new InMemoryProvisioner(true, "host1.yahoo.com", "host2.yahoo.com"))
+ .applicationPackage(applicationPackage)
+ .properties(new DeployProperties.Builder()
+ .multitenant(true)
+ .hostedVespa(true)
+ .build())
+ .build());
+ assertEquals(1, model.getHostSystem().getHosts().size());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void renderers_named_JsonRenderer_are_not_allowed() throws IOException, SAXException {
+ createModel(root, generateContainerElementWithRenderer("JsonRenderer"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void renderers_named_DefaultRenderer_are_not_allowed() throws IOException, SAXException {
+ createModel(root, generateContainerElementWithRenderer("DefaultRenderer"));
+ }
+
+ @Test
+ public void renderers_named_something_else_are_allowed() throws IOException, SAXException {
+ createModel(root, generateContainerElementWithRenderer("my-little-renderer"));
+ }
+
+ @Test
+ public void vip_status_handler_uses_file_for_hosted_vespa() throws Exception {
+ String servicesXml = "<services>" +
+ "<jdisc version='1.0'>" +
+ nodesXml +
+ "</jdisc>" +
+ "</services>";
+
+ ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withServices(servicesXml).build();
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), new DeployState.Builder()
+ .applicationPackage(applicationPackage)
+ .properties(new DeployProperties.Builder()
+ .hostedVespa(true)
+ .build())
+ .build());
+
+ AbstractConfigProducerRoot modelRoot = model.getRoot();
+ VipStatusConfig vipStatusConfig = modelRoot.getConfig(VipStatusConfig.class, "jdisc/component/status.html-status-handler");
+ assertTrue(vipStatusConfig.accessdisk());
+ assertEquals(ContainerModelBuilder.HOSTED_VESPA_STATUS_FILE, vipStatusConfig.statusfile());
+ }
+
+ private Element generateContainerElementWithRenderer(String rendererId) {
+ return DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <search>",
+ String.format(" <renderer id='%s'/>", rendererId),
+ " </search>",
+ "</jdisc>");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java
new file mode 100644
index 00000000000..b5410f332f6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTestBase.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.component.Component;
+import com.yahoo.vespa.model.container.search.ContainerSearch;
+import com.yahoo.vespa.model.search.AbstractSearchCluster;
+import org.junit.Before;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/**
+ * Utility functions for testing the ContainerModelBuilder
+ *
+ * @author gjoranv
+ * @since 5.5
+ */
+public abstract class ContainerModelBuilderTestBase {
+
+ public static final String nodesXml =
+ " <nodes>" +
+ " <node hostalias='mockhost' />" +
+ " </nodes>";
+ protected MockRoot root;
+
+ public static void createModel(MockRoot root, Element... containerElems) throws SAXException, IOException {
+ for (Element containerElem : containerElems) {
+ ContainerModel model = new ContainerModelBuilder(false, ContainerModelBuilder.Networking.enable).build(DeployState.createTestState(), null, root, containerElem);
+ ContainerCluster cluster = model.getCluster();
+ generateDefaultSearchChains(cluster);
+ }
+ root.freezeModelTopology();
+ }
+
+ private static void generateDefaultSearchChains(ContainerCluster cluster) {
+ ContainerSearch search = cluster.getSearch();
+ if (search != null)
+ search.initializeSearchChains(Collections.<String, AbstractSearchCluster>emptyMap());
+ }
+
+ @Before
+ public void prepareTest() throws Exception {
+ root = new MockRoot("root");
+ }
+
+ protected ComponentsConfig componentsConfig() {
+ return root.getConfig(ComponentsConfig.class, "default");
+ }
+
+ protected ComponentsConfig.Components getComponent(ComponentsConfig componentsConfig, String id) {
+ for (ComponentsConfig.Components component : componentsConfig.components()) {
+ if (component.id().equals(id))
+ return component;
+ }
+ return null;
+ }
+
+ public ContainerCluster getContainerCluster(String clusterId) {
+ return (ContainerCluster) root.getChildren().get(clusterId);
+ }
+
+ public Component<?, ?> getContainerComponent(String clusterId, String componentId) {
+ return getContainerCluster(clusterId).getComponentsMap().get(
+ ComponentId.fromString(componentId));
+ }
+
+ // TODO: will not work with multiple instances of the same class
+ public Component<?, ?> getContainerComponentNested(String clusterId, String componentId) {
+ ComponentId id = ComponentId.fromString(componentId);
+ for (Component<?,?> component : getContainerCluster(clusterId).getAllComponents())
+ if (id.equals(component.getComponentId()))
+ return component;
+ return null;
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/DocprocBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/DocprocBuilderTest.java
new file mode 100644
index 00000000000..f38e831cdbb
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/DocprocBuilderTest.java
@@ -0,0 +1,226 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.docproc.DocprocConfig;
+import com.yahoo.config.docproc.SchemamappingConfig;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.BundlesConfig;
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.container.core.ChainsConfig;
+import com.yahoo.container.jdisc.ContainerMbusConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.search.config.QrStartConfig;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.container.docproc.DocumentProcessor;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder.Networking;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.core.IsNull.notNullValue;
+import static org.junit.Assert.*;
+
+/**
+ * @author einarmr
+ * @author gjoranv
+ * @since 5.1.9
+ */
+public class DocprocBuilderTest extends DomBuilderTest {
+
+ private ContainerCluster cluster;
+ private DocumentmanagerConfig documentmanagerConfig;
+ private ContainerMbusConfig containerMbusConfig;
+ private ComponentsConfig componentsConfig;
+ private ChainsConfig chainsConfig;
+ private BundlesConfig bundlesConfig;
+ private SchemamappingConfig schemamappingConfig;
+ private DocprocConfig docprocConfig;
+ private QrStartConfig qrStartConfig;
+
+ @Before
+ public void setupCluster() {
+ ContainerModel model = new ContainerModelBuilder(false, Networking.disable).build(DeployState.createTestState(), null, root, servicesXml());
+ cluster = model.getCluster();
+ cluster.getDocproc().getChains().addServersAndClientsForChains();
+ root.freezeModelTopology();
+
+ containerMbusConfig = root.getConfig(ContainerMbusConfig.class, cluster.getContainers().get(0).getConfigId());
+ componentsConfig = root.getConfig(ComponentsConfig.class, cluster.getConfigId());
+ chainsConfig = root.getConfig(ChainsConfig.class,
+ cluster.getConfigId() + "/component/com.yahoo.docproc.jdisc.DocumentProcessingHandler");
+
+ documentmanagerConfig = root.getConfig(DocumentmanagerConfig.class, cluster.getConfigId());
+ bundlesConfig = root.getConfig(BundlesConfig.class, cluster.getConfigId());
+ schemamappingConfig = root.getConfig(SchemamappingConfig.class, cluster.getContainers().get(0).getConfigId());
+ qrStartConfig = root.getConfig(QrStartConfig.class, cluster.getConfigId());
+ docprocConfig = root.getConfig(DocprocConfig.class, cluster.getConfigId());
+ }
+
+ private Element servicesXml() {
+ return parse(
+ "<jdisc id='banan' version='1.0'>",
+ " <nodes>",
+ " <node hostalias='mockhost' baseport='1500' />",
+ " </nodes>",
+ " <document-processing compressdocuments='true' preferlocalnode='true' numnodesperclient='2' maxqueuebytesize='100m' maxmessagesinqueue='300' maxqueuewait='200'>",
+ " <documentprocessor id='docproc1' class='com.yahoo.Docproc1' bundle='docproc1bundle'/>",
+ " <chain id='chein'>",
+ " <documentprocessor id='docproc2'/>",
+ " </chain>",
+ " </document-processing>",
+ "</jdisc>");
+ }
+
+ // TODO: re-enable assertions when the appropriate attributes are handled by the builder
+ @Test
+ public void testDocprocCluster() {
+ assertThat(cluster.getName(), is("banan"));
+ assertThat(cluster.getDocproc().isCompressDocuments(), is(true));
+ //assertThat(cluster.getContainerDocproc().isPreferLocalNode(), is(true));
+ //assertThat(cluster.getContainerDocproc().getNumNodesPerClient(), is(2));
+ List<Container> services = cluster.getContainers();
+ assertThat(services.size(), is(1));
+ Container service = services.get(0);
+ assertThat(service, notNullValue());
+
+ Map<String, DocprocChain> chains = new HashMap<>();
+ for (DocprocChain chain : cluster.getDocprocChains().allChains().allComponents()) {
+ chains.put(chain.getId().stringValue(), chain);
+ }
+ assertThat(chains.size(), is(1));
+
+ DocprocChain chain = chains.get("chein");
+ assertThat(chain.getId().stringValue(), is("chein"));
+ assertThat(chain.getInnerComponents().size(), is(1));
+ DocumentProcessor processor = chain.getInnerComponents().iterator().next();
+ assertThat(processor.getComponentId().stringValue(), is("docproc2"));
+ }
+
+ @Test
+ public void testDocumentManagerConfig() {
+ assertThat(documentmanagerConfig.enablecompression(), is(true));
+ }
+
+ @Test
+ public void testDocprocConfig() {
+ assertThat(docprocConfig.maxqueuetimems(), is(200000));
+
+ }
+
+ @Test
+ public void testContainerMbusConfig() {
+ assertThat(containerMbusConfig.enabled(), is(true));
+ assertTrue(containerMbusConfig.port() >= HostResource.BASE_PORT);
+ assertThat(containerMbusConfig.maxpendingcount(), is(300));
+ assertThat(containerMbusConfig.maxpendingsize(), is(100));
+ }
+
+ @Test
+ public void testComponentsConfig() {
+ Map<String, ComponentsConfig.Components> components = new HashMap<>();
+ for (ComponentsConfig.Components component : componentsConfig.components()) {
+ System.err.println(component.id());
+ components.put(component.id(), component);
+ }
+
+ ComponentsConfig.Components docprocHandler = components.get("com.yahoo.docproc.jdisc.DocumentProcessingHandler");
+ assertThat(docprocHandler.id(), is("com.yahoo.docproc.jdisc.DocumentProcessingHandler"));
+ assertThat(docprocHandler.configId(), is("banan/component/com.yahoo.docproc.jdisc.DocumentProcessingHandler"));
+ assertThat(docprocHandler.classId(), is("com.yahoo.docproc.jdisc.DocumentProcessingHandler"));
+ assertThat(docprocHandler.bundle(), is("container-search-and-docproc"));
+
+ ComponentsConfig.Components docproc1 = components.get("docproc1");
+ assertThat(docproc1.id(), is("docproc1"));
+ assertThat(docproc1.configId(), is("banan/docprocchains/component/docproc1"));
+ assertThat(docproc1.classId(), is("com.yahoo.Docproc1"));
+ assertThat(docproc1.bundle(), is("docproc1bundle"));
+
+ ComponentsConfig.Components docproc2 = components.get("docproc2@chein");
+ assertThat(docproc2.id(), is("docproc2@chein"));
+ assertThat(docproc2.configId(), is("banan/docprocchains/chain/chein/component/docproc2"));
+ assertThat(docproc2.classId(), is("docproc2"));
+ assertThat(docproc2.bundle(), is("docproc2"));
+/*
+ ComponentsConfig.Components health = components.get("com.yahoo.container.jdisc.state.StateHandler");
+ assertThat(health.id(), is("com.yahoo.container.jdisc.state.StateHandler"));
+ assertThat(health.classId(), is("com.yahoo.container.jdisc.state.StateHandler"));
+ assertThat(health.bundle(), is("com.yahoo.container.jdisc.state.StateHandler"));
+*/
+ ComponentsConfig.Components sourceClient = components.get("source@MbusClient");
+ assertNotNull(sourceClient);
+ assertThat(sourceClient.classId(), is("com.yahoo.container.jdisc.messagebus.MbusClientProvider"));
+ assertThat(sourceClient.bundle(), is("com.yahoo.container.jdisc.messagebus.MbusClientProvider"));
+
+ ComponentsConfig.Components intermediateClient = components.get("chain.chein@MbusClient");
+ assertNotNull(intermediateClient);
+ assertThat(intermediateClient.classId(), is("com.yahoo.container.jdisc.messagebus.MbusClientProvider"));
+ assertThat(intermediateClient.bundle(), is("com.yahoo.container.jdisc.messagebus.MbusClientProvider"));
+ }
+
+ @Test
+ public void testChainsConfig() {
+ Map<String, ChainsConfig.Components> components = new HashMap<>();
+ for (ChainsConfig.Components component : chainsConfig.components()) {
+ components.put(component.id(), component);
+ }
+
+ assertThat(components.size(), is(2));
+
+ ChainsConfig.Components docproc1 = components.get("docproc1");
+ assertThat(docproc1.id(), is("docproc1"));
+ assertThat(docproc1.dependencies().provides().size(), is(0));
+ assertThat(docproc1.dependencies().before().size(), is(0));
+ assertThat(docproc1.dependencies().after().size(), is(0));
+
+ ChainsConfig.Components docproc2 = components.get("docproc2@chein");
+ assertThat(docproc2.id(), is("docproc2@chein"));
+ assertThat(docproc2.dependencies().provides().size(), is(0));
+ assertThat(docproc2.dependencies().before().size(), is(0));
+ assertThat(docproc2.dependencies().after().size(), is(0));
+
+ Map<String, ChainsConfig.Chains> chainsMap = new HashMap<>();
+ for (ChainsConfig.Chains chain : chainsConfig.chains()) {
+ chainsMap.put(chain.id(), chain);
+ }
+
+ assertThat(chainsMap.size(), is(1));
+ assertThat(chainsMap.get("chein").id(), is("chein"));
+ assertThat(chainsMap.get("chein").components().size(), is(1));
+ assertThat(chainsMap.get("chein").components(0), is("docproc2@chein"));
+ assertThat(chainsMap.get("chein").inherits().size(), is(0));
+ assertThat(chainsMap.get("chein").excludes().size(), is(0));
+ assertThat(chainsMap.get("chein").phases().size(), is(0));
+ }
+
+ @Test
+ public void testBundlesConfig() {
+ assertThat(bundlesConfig.bundle().size(), is(0));
+ }
+
+ @Test
+ public void testSchemaMappingConfig() {
+ assertThat(schemamappingConfig.fieldmapping().size(), is(0));
+ }
+
+ @Test
+ public void testQrStartConfig() {
+ QrStartConfig.Jvm jvm = qrStartConfig.jvm();
+ assertThat(jvm.server(), is(true));
+ assertThat(jvm.verbosegc(), is(false));
+ assertThat(jvm.gcopts(), is(""));
+ assertThat(jvm.heapsize(), is(1536));
+ assertThat(jvm.stacksize(), is(512));
+ assertThat(qrStartConfig.ulimitv(), is(""));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java
new file mode 100644
index 00000000000..453b8d0ffa6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java
@@ -0,0 +1,234 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.container.jdisc.FilterBindingsProvider;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.http.JettyHttpServer;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author einarmr
+ * @since 5.15
+ */
+public class JettyContainerModelBuilderTest extends ContainerModelBuilderTestBase {
+
+ @Test
+ public void verify_that_overriding_connector_options_works() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0' jetty='true'>\n" +
+ " <http>\n" +
+ " <server id='bananarama' port='4321'>\n" +
+ " <config name='jdisc.http.connector'>\n" +
+ " <requestHeaderSize>300000</requestHeaderSize>\n" +
+ " <headerCacheSize>300000</headerCacheSize>\n" +
+ " </config>\n" +
+ " </server>\n" +
+ " </http>\n" +
+ nodesXml +
+ "</jdisc>\n"
+ );
+ createModel(root, clusterElem);
+ ConnectorConfig.Builder connectorConfigBuilder = new ConnectorConfig.Builder();
+ ConnectorConfig cfg = root.getConfig(ConnectorConfig.class, "default/http/jdisc-jetty/bananarama");
+ assertThat(cfg.requestHeaderSize(), is(300000));
+ assertThat(cfg.headerCacheSize(), is(300000));
+ }
+
+ @Test
+ public void verify_that_enabling_jetty_works() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0' jetty='true'>" +
+ nodesXml +
+ "</jdisc>"
+ );
+ createModel(root, clusterElem);
+ assertJettyServerInConfig();
+ }
+
+ @Test
+ public void verify_that_enabling_jetty_works_for_custom_http_servers() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0' jetty='true'>",
+ " <http>",
+ " <server port='9000' id='foo' />",
+ " </http>",
+ nodesXml,
+ "</jdisc>" );
+ createModel(root, clusterElem);
+ assertJettyServerInConfig();
+ }
+
+ @Test
+ public void verifyThatJettyHttpServerHasFilterBindingsProvider() throws Exception {
+ final Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0' jetty='true'>",
+ nodesXml,
+ "</jdisc>" );
+ createModel(root, clusterElem);
+
+ final ComponentsConfig.Components jettyHttpServerComponent = extractComponentByClassName(
+ containerComponentsConfig(), com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName());
+ assertThat(jettyHttpServerComponent, is(not(nullValue())));
+
+ final ComponentsConfig.Components filterBindingsProviderComponent = extractComponentByClassName(
+ containerComponentsConfig(), FilterBindingsProvider.class.getName());
+ assertThat(filterBindingsProviderComponent, is(not(nullValue())));
+
+ final ComponentsConfig.Components.Inject filterBindingsProviderInjection = extractInjectionById(
+ jettyHttpServerComponent, filterBindingsProviderComponent.id());
+ assertThat(filterBindingsProviderInjection, is(not(nullValue())));
+ }
+
+ @Test
+ public void verifyThatJettyHttpServerHasFilterBindingsProviderForCustomHttpServers() throws Exception {
+ final Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0' jetty='true'>",
+ " <http>",
+ " <server port='9000' id='foo' />",
+ " </http>",
+ nodesXml,
+ "</jdisc>" );
+ createModel(root, clusterElem);
+
+ final ComponentsConfig.Components jettyHttpServerComponent = extractComponentByClassName(
+ clusterComponentsConfig(), com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName());
+ assertThat(jettyHttpServerComponent, is(not(nullValue())));
+
+ final ComponentsConfig.Components filterBindingsProviderComponent = extractComponentByClassName(
+ clusterComponentsConfig(), FilterBindingsProvider.class.getName());
+ assertThat(filterBindingsProviderComponent, is(not(nullValue())));
+
+ final ComponentsConfig.Components.Inject filterBindingsProviderInjection = extractInjectionById(
+ jettyHttpServerComponent, filterBindingsProviderComponent.id());
+ assertThat(filterBindingsProviderInjection, is(not(nullValue())));
+ }
+
+ @Test
+ public void verify_that_old_http_config_override_inside_server_tag_works() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0' jetty='true'>",
+ " <http>",
+ " <server port='9000' id='foo'>",
+ " <config name=\"container.jdisc.config.http-server\">",
+ " <tcpKeepAliveEnabled>true</tcpKeepAliveEnabled>",
+ " <tcpNoDelayEnabled>false</tcpNoDelayEnabled>",
+ " <tcpListenBacklogLength>2</tcpListenBacklogLength>",
+ " <idleConnectionTimeout>34.1</idleConnectionTimeout>",
+ " <soLinger>42.2</soLinger>",
+ " <sendBufferSize>1234</sendBufferSize>",
+ " <maxHeaderSize>4321</maxHeaderSize>",
+ " <ssl>",
+ " <enabled>true</enabled>",
+ " <keyStoreType>JKS</keyStoreType>",
+ " <keyStorePath>apple</keyStorePath>",
+ " <trustStorePath>grape</trustStorePath>",
+ " <keyDBKey>tomato</keyDBKey>",
+ " <algorithm>onion</algorithm>",
+ " <protocol>carrot</protocol>",
+ " </ssl>",
+ " </config>",
+ " </server>",
+ " </http>",
+ nodesXml,
+ "</jdisc>" );
+ createModel(root, clusterElem);
+ ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default");
+ List<JettyHttpServer> jettyServers = cluster.getChildrenByTypeRecursive(JettyHttpServer.class);
+
+ assertThat(jettyServers.size(), is(1));
+
+ JettyHttpServer server = jettyServers.get(0);
+ assertThat(server.model.bundleInstantiationSpec.classId.toString(),
+ is(com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName()));
+ assertThat(server.model.bundleInstantiationSpec.bundle.toString(), is("jdisc_http_service"));
+ assertThat(server.getConnectorFactories().size(), is(1));
+
+ ConnectorConfig.Builder connectorConfigBuilder = new ConnectorConfig.Builder();
+ server.getConnectorFactories().get(0).getConfig(connectorConfigBuilder);
+ ConnectorConfig connector = new ConnectorConfig(connectorConfigBuilder);
+ assertThat(connector.name(), equalTo("foo"));
+ assertThat(connector.tcpKeepAliveEnabled(), equalTo(true));
+ assertThat(connector.tcpNoDelay(), equalTo(false));
+ assertThat(connector.acceptQueueSize(), equalTo(2));
+ assertThat(connector.idleTimeout(), equalTo(34.1));
+ assertThat(connector.soLingerTime(), equalTo(42));
+ assertThat(connector.outputBufferSize(), equalTo(1234));
+ assertThat(connector.headerCacheSize(), equalTo(4321));
+ assertThat(connector.ssl().enabled(), equalTo(true));
+ assertThat(connector.ssl().keyStoreType(), equalTo(KeyStoreType.Enum.JKS));
+ assertThat(connector.ssl().keyStorePath(), equalTo("apple"));
+ assertThat(connector.ssl().trustStorePath(), equalTo("grape"));
+ assertThat(connector.ssl().keyDbKey(), equalTo("tomato"));
+ assertThat(connector.ssl().sslKeyManagerFactoryAlgorithm(), equalTo("onion"));
+ assertThat(connector.ssl().protocol(), equalTo("carrot"));
+
+ assertThat(
+ extractComponentByClassName(
+ clusterComponentsConfig(),
+ com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName()),
+ is(not(nullValue())));
+ }
+
+ private void assertJettyServerInConfig() {
+ ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default");
+ List<JettyHttpServer> jettyServers = cluster.getChildrenByTypeRecursive(JettyHttpServer.class);
+
+ assertThat(jettyServers.size(), is(1));
+
+ JettyHttpServer server = jettyServers.get(0);
+ assertThat(server.model.bundleInstantiationSpec.classId.toString(),
+ is(com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName()));
+ assertThat(server.model.bundleInstantiationSpec.bundle.toString(), is("jdisc_http_service"));
+ assertThat(server.getConnectorFactories().size(), is(1));
+
+ assertThat(
+ extractComponentByClassName(
+ containerComponentsConfig(),
+ com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName()),
+ is(not(nullValue())));
+ }
+
+ private static ComponentsConfig.Components extractComponentByClassName(
+ final ComponentsConfig componentsConfig, final String className) {
+ for (final ComponentsConfig.Components component : componentsConfig.components()) {
+ if (className.equals(component.classId())) {
+ return component;
+ }
+ }
+ return null;
+ }
+
+ private static ComponentsConfig.Components.Inject extractInjectionById(
+ final ComponentsConfig.Components component, final String id) {
+ for (final ComponentsConfig.Components.Inject injection : component.inject()) {
+ if (id.equals(injection.id())) {
+ return injection;
+ }
+ }
+ return null;
+ }
+
+ private ComponentsConfig containerComponentsConfig() {
+ final ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default");
+ return root.getConfig(
+ ComponentsConfig.class,
+ cluster.getContainers().get(0).getConfigId());
+ }
+
+ private ComponentsConfig clusterComponentsConfig() {
+ return componentsConfig();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/SearchBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/SearchBuilderTest.java
new file mode 100644
index 00000000000..269edf6c5ad
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/SearchBuilderTest.java
@@ -0,0 +1,194 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml;
+
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest;
+import com.yahoo.container.core.ChainsConfig;
+import com.yahoo.container.jdisc.JdiscBindingsConfig;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.Container;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+import static com.yahoo.test.Matchers.hasItemWithMethod;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.*;
+
+/**
+ * @author gjoranv
+ * @since 5.1.10
+ */
+public class SearchBuilderTest extends ContainerModelBuilderTestBase {
+
+ private ChainsConfig chainsConfig() {
+ return root.getConfig(ChainsConfig.class, "default/component/com.yahoo.search.handler.SearchHandler");
+ }
+
+
+ @Test
+ public void search_handler_bindings_can_be_overridden() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <search>",
+ " <binding>binding0</binding>",
+ " <binding>binding1</binding>",
+ " </search>",
+ nodesXml,
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+
+ String discBindingsConfig = root.getConfig(JdiscBindingsConfig.class, "default").toString();
+ assertThat(discBindingsConfig, containsString(".serverBindings[0] \"binding0\""));
+ assertThat(discBindingsConfig, containsString(".serverBindings[1] \"binding1\""));
+ assertThat(discBindingsConfig, not(containsString("/search/*")));
+ }
+
+ @Test
+ public void search_handler_bindings_can_be_disabled() throws Exception {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <search>",
+ " <binding/>",
+ " </search>",
+ nodesXml,
+ "</jdisc>");
+
+ createModel(root, clusterElem);
+
+ String discBindingsConfig = root.getConfig(JdiscBindingsConfig.class, "default").toString();
+ assertThat(discBindingsConfig, not(containsString("/search/*")));
+ }
+
+ // TODO: remove test when all containers are named 'container'
+ @Test
+ public void cluster_with_only_search_gets_qrserver_as_service_name() throws Exception {
+ createClusterWithOnlyDefaultChains();
+ ContainerCluster cluster = (ContainerCluster)root.getChildren().get("default");
+ assertThat(cluster.getContainers().get(0).getServiceName(), is("qrserver"));
+ }
+
+ @Test
+ public void empty_search_element_gives_default_chains() throws Exception {
+ createClusterWithOnlyDefaultChains();
+ assertThat(chainsConfig().chains(), hasItemWithMethod("vespaPhases", "id"));
+ assertThat(chainsConfig().chains(), hasItemWithMethod("native", "id"));
+ assertThat(chainsConfig().chains(), hasItemWithMethod("vespa", "id"));
+ }
+
+ private void createClusterWithOnlyDefaultChains() throws SAXException, IOException {
+ Element containerElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <search/>",
+ " <nodes>",
+ " <node hostalias='mockhost' />",
+ " </nodes>",
+ "</jdisc>");
+
+ createModel(root, containerElem);
+ }
+
+ @Test
+ public void manually_setting_up_search_handler_is_forbidden() throws IOException, SAXException {
+ try {
+ Element clusterElem = DomBuilderTest.parse(
+ "<jdisc id='default' version='1.0'>",
+ " <handler id='com.yahoo.search.handler.SearchHandler' />",
+ nodesXml,
+ " </jdisc>");
+
+
+ createModel(root, clusterElem);
+ fail("Expected exception");
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Setting up com.yahoo.search.handler.SearchHandler manually is not supported"));
+ }
+ }
+
+ @Test
+ public void cluster_is_connected_to_content_clusters() throws Exception {
+ String hosts = hostsXml();
+
+ String services = "" +
+ "<services>"+
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='mockhost'/>" +
+ " </admin>" +
+ " <jdisc version='1.0' id='container'>"+
+ " <search>" +
+ " <chain id='mychain' inherits='vespa'/>" +
+ " </search>" +
+ " <nodes>"+
+ " <node hostalias=\"mockhost\" />"+
+ " </nodes>"+
+ " </jdisc>"+
+ contentXml() +
+ "</services>";
+
+ VespaModel model = getVespaModelWithMusic(hosts, services);
+
+ ContainerCluster cluster = model.getContainerClusters().get("container");
+ assertFalse(cluster.getSearchChains().localProviders().isEmpty());
+ }
+
+ @Test
+ public void cluster_is_connected_to_search_clusters() throws Exception {
+ String hosts = hostsXml();
+
+ String services = "" +
+ "<services>"+
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='mockhost'/>" +
+ " </admin>" +
+ " <jdisc version='1.0' id='container'>"+
+ " <search>" +
+ " <chain id='mychain' inherits='vespa'/>" +
+ " </search>" +
+ " <nodes>"+
+ " <node hostalias=\"mockhost\" />"+
+ " </nodes>"+
+ " </jdisc>"+
+ contentXml() +
+ "</services>";
+
+ VespaModel model = getVespaModelWithMusic(hosts, services);
+
+ ContainerCluster cluster = model.getContainerClusters().get("container");
+ assertFalse(cluster.getSearchChains().localProviders().isEmpty());
+ }
+
+
+ private VespaModel getVespaModelWithMusic(String hosts, String services) throws ParseException {
+ return new VespaModelCreatorWithMockPkg(hosts, services, ApplicationPackageUtils.generateSearchDefinitions("music")).create();
+ }
+
+ private String hostsXml() {
+ return "" +
+ "<hosts> " +
+ " <host name=\"node0\">" +
+ " <alias>mockhost</alias>" +
+ " </host>" +
+ "</hosts>";
+ }
+
+ private String contentXml() {
+ return " <content version=\"1.0\" id='content'>"+
+ " <documents>\n" +
+ " <document type=\"music\" mode='index'/>\n" +
+ " </documents>\n" +
+ " <redundancy>3</redundancy>"+
+ " <group>"+
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>"+
+ " </group>"+
+ " </content>";
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/ClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/ClusterTest.java
new file mode 100644
index 00000000000..bcb113687ec
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/ClusterTest.java
@@ -0,0 +1,762 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.model.test.TestRoot;
+import com.yahoo.vespa.config.content.core.StorDistributormanagerConfig;
+import com.yahoo.vespa.config.content.StorFilestorConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+import com.yahoo.metrics.MetricsmanagerConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.engines.ProtonEngine;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+public class ClusterTest extends ContentBaseTest {
+
+ private final static String HOSTS = "<admin version='2.0'><adminserver hostalias='mockhost' /></admin>";
+
+
+ ContentCluster parse(String xml) {
+ xml = HOSTS + xml;
+ TestRoot root = new TestDriver().buildModel(xml);
+ return root.getConfigModels(Content.class).get(0).getCluster();
+ }
+
+ @Test
+ public void testRedundancy() {
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ parse("" +
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <proton>" +
+ " <searchable-copies>3</searchable-copies>" +
+ " </proton>" +
+ " </engine>" +
+ " <redundancy reply-after=\"4\">5</redundancy>\n" +
+ " <group>" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>\"" +
+ " <node hostalias=\"mockhost\" distribution-key=\"1\"/>\"" +
+ " <node hostalias=\"mockhost\" distribution-key=\"2\"/>\"" +
+ " <node hostalias=\"mockhost\" distribution-key=\"3\"/>\"" +
+ " <node hostalias=\"mockhost\" distribution-key=\"4\"/>\"" +
+ " </group>" +
+ "</content>"
+ ).getConfig(builder);
+
+ StorDistributionConfig config = new StorDistributionConfig(builder);
+ assertEquals(4, config.initial_redundancy());
+ assertEquals(5, config.redundancy());
+ assertEquals(3, config.ready_copies());
+ }
+
+ @Test
+ public void testNoId() {
+ ContentCluster c = parse(
+ "<content version=\"1.0\">\n" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents/>" +
+ " <redundancy reply-after=\"4\">5</redundancy>\n" +
+ " <group>" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>\"" +
+ " </group>" +
+ "</content>"
+ );
+
+ assertEquals("content", c.getName());
+ }
+
+ @Test
+ public void testRedundancyDefaults() {
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>\"" +
+ " <node hostalias=\"mockhost\" distribution-key=\"1\"/>\"" +
+ " <node hostalias=\"mockhost\" distribution-key=\"2\"/>\"" +
+ " </group>" +
+ "</content>"
+ ).getConfig(builder);
+
+ StorDistributionConfig config = new StorDistributionConfig(builder);
+ assertEquals(2, config.initial_redundancy());
+ assertEquals(3, config.redundancy());
+ assertEquals(2, config.ready_copies());
+ }
+
+ @Test
+ public void testEndToEnd() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"configserver\" />\n" +
+ " <logserver hostalias=\"logserver\" />\n" +
+ " <slobroks>\n" +
+ " <slobrok hostalias=\"configserver\" />\n" +
+ " <slobrok hostalias=\"logserver\" />\n" +
+ " </slobroks>\n" +
+ " <cluster-controllers>\n" +
+ " <cluster-controller hostalias=\"configserver\"/>" +
+ " <cluster-controller hostalias=\"configserver2\"/>" +
+ " <cluster-controller hostalias=\"configserver3\"/>" +
+ " </cluster-controllers>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode=\"index\"/>\n" +
+ " <document type=\"type2\" mode=\"index\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ " <tuning>" +
+ " <cluster-controller>\n" +
+ " <init-progress-time>34567</init-progress-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1", "type2");
+ VespaModel model = (new VespaModelCreatorWithMockPkg(null, xml, sds)).create();
+ assertEquals(2, model.getContentClusters().get("bar").getDocumentDefinitions().size());
+ ContainerCluster cluster = model.getAdmin().getClusterControllers();
+ assertEquals(3, cluster.getContainers().size());
+ }
+
+ @Test
+ public void testEndToEndOneNode() throws Exception {
+ String services =
+ "<?xml version='1.0' encoding='UTF-8' ?>" +
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node1'/>" +
+ " </admin>" +
+ " <jdisc id='default' version='1.0'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='node1'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content id='storage' version='1.0'>" +
+ " <redundancy>2</redundancy>" +
+ " <group>" +
+ " <node distribution-key='0' hostalias='node1'/>" +
+ " <node distribution-key='1' hostalias='node1'/>" +
+ " </group>" +
+ " <tuning>" +
+ " <cluster-controller>" +
+ " <transition-time>0</transition-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " <documents>" +
+ " <document mode='store-only' type='type1'/>" +
+ " </documents>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " </content>" +
+ " </services>";
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1");
+ VespaModel model = (new VespaModelCreatorWithMockPkg(null, services, sds)).create();
+ assertEquals(1, model.getContentClusters().get("storage").getDocumentDefinitions().size());
+ ContainerCluster cluster = model.getAdmin().getClusterControllers();
+ assertEquals(1, cluster.getContainers().size());
+ }
+
+ @Test
+ public void testSearchTuning() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"node0\" />\n" +
+ " <cluster-controllers>\n" +
+ " <cluster-controller hostalias=\"node0\"/>" +
+ " </cluster-controllers>\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode='index'/>\n" +
+ " <document type=\"type2\" mode='index'/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0'/>" +
+ " </group>" +
+ " <tuning>\n" +
+ " <cluster-controller>" +
+ " <init-progress-time>34567</init-progress-time>" +
+ " </cluster-controller>" +
+ " </tuning>" +
+ " </content>" +
+ "\n" +
+ "</services>";
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1", "type2");
+ VespaModel model = new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
+
+ assertTrue(model.getContentClusters().get("bar").getPersistence() instanceof ProtonEngine.Factory);
+
+ {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ model.getConfig(builder, "bar/distributor/0");
+ StorDistributormanagerConfig config = new StorDistributormanagerConfig(builder);
+ assertEquals(false, config.inlinebucketsplitting());
+ }
+
+ {
+ StorFilestorConfig.Builder builder = new StorFilestorConfig.Builder();
+ model.getConfig(builder, "bar/storage/0");
+ StorFilestorConfig config = new StorFilestorConfig(builder);
+ assertEquals(false, config.enable_multibit_split_optimalization());
+ }
+ }
+
+ @Test
+ public void testRedundancyRequired() throws Exception {
+ String xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+ "<services>\n" +
+ "\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"node0\" />\n" +
+ " </admin>\n" +
+ " <content version='1.0' id='bar'>" +
+ " <documents>" +
+ " <document type=\"type1\" mode='index'/>\n" +
+ " </documents>\n" +
+ " <group>\n" +
+ " <node hostalias='node0' distribution-key='0'/>\n" +
+ " </group>\n" +
+ " </content>\n" +
+ "</services>\n";
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1", "type2");
+ try{
+ new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
+ assertTrue("Deploying without redundancy should fail", false);
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("missing required element \"redundancy\""));
+ }
+ }
+
+ @Test
+ public void testRedundancyFinalLessThanInitial() {
+ try {
+ parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <redundancy reply-after=\"4\">2</redundancy>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ "</content>"
+ );
+ fail("no exception thrown");
+ } catch (Exception e) {
+ }
+ }
+
+ @Test
+ public void testReadyTooHigh() {
+ try {
+ parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <engine>" +
+ " <proton>" +
+ " <searchable-copies>3</searchable-copies>" +
+ " </proton>" +
+ " </engine>" +
+ " <redundancy>2</redundancy>\n" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ "</content>"
+ );
+ fail("no exception thrown");
+ } catch (Exception e) {
+ }
+ }
+
+ FleetcontrollerConfig getFleetControllerConfig(String xml) {
+ ContentCluster cluster = parse(xml);
+
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ cluster.getConfig(builder);
+ cluster.getClusterControllerConfig().getConfig(builder);
+ return new FleetcontrollerConfig(builder);
+ }
+
+ @Test
+ public void testFleetControllerOverride()
+ {
+ {
+ FleetcontrollerConfig config = getFleetControllerConfig(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</content>"
+ );
+
+ assertEquals(0, config.min_storage_up_ratio(), 0.01);
+ assertEquals(0, config.min_distributor_up_ratio(), 0.01);
+ assertEquals(1, config.min_storage_up_count());
+ assertEquals(1, config.min_distributors_up_count());
+ }
+
+ {
+ FleetcontrollerConfig config = getFleetControllerConfig(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"2\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"3\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"4\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"5\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</content>"
+ );
+
+ assertNotSame(0, config.min_storage_up_ratio());
+ }
+ }
+
+ @Test
+ public void testImplicitDistributionBits()
+ {
+ ContentCluster cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</content>"
+ );
+
+ {
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ cluster.getConfig(builder);
+ cluster.getClusterControllerConfig().getConfig(builder);
+ FleetcontrollerConfig config = new FleetcontrollerConfig(builder);
+ assertEquals(8, config.ideal_distribution_bits());
+ }
+
+ {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ cluster.getConfig(builder);
+ StorDistributormanagerConfig config = new StorDistributormanagerConfig(builder);
+ assertEquals(8, config.minsplitcount());
+ }
+ cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</content>"
+ );
+
+ {
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ cluster.getConfig(builder);
+ cluster.getClusterControllerConfig().getConfig(builder);
+ FleetcontrollerConfig config = new FleetcontrollerConfig(builder);
+ assertEquals(8, config.ideal_distribution_bits());
+ }
+
+ {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ cluster.getConfig(builder);
+ StorDistributormanagerConfig config = new StorDistributormanagerConfig(builder);
+ assertEquals(8, config.minsplitcount());
+ }
+ }
+
+ @Test
+ public void testExplicitDistributionBits()
+ {
+ ContentCluster cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " <tuning>\n" +
+ " <distribution type=\"strict\"/>\n" +
+ " </tuning>\n" +
+ "</content>"
+ );
+
+ {
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ cluster.getConfig(builder);
+ cluster.getClusterControllerConfig().getConfig(builder);
+ FleetcontrollerConfig config = new FleetcontrollerConfig(builder);
+ assertEquals(8, config.ideal_distribution_bits());
+ }
+
+ {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ cluster.getConfig(builder);
+ StorDistributormanagerConfig config = new StorDistributormanagerConfig(builder);
+ assertEquals(8, config.minsplitcount());
+ }
+ cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " <tuning>\n" +
+ " <distribution type=\"loose\"/>\n" +
+ " </tuning>\n" +
+ "</content>"
+ );
+
+ {
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ cluster.getConfig(builder);
+ cluster.getClusterControllerConfig().getConfig(builder);
+ FleetcontrollerConfig config = new FleetcontrollerConfig(builder);
+ assertEquals(8, config.ideal_distribution_bits());
+ }
+
+ {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ cluster.getConfig(builder);
+ StorDistributormanagerConfig config = new StorDistributormanagerConfig(builder);
+ assertEquals(8, config.minsplitcount());
+ }
+ }
+
+ @Test
+ public void testGenerateSearchNodes()
+ {
+ ContentCluster cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</content>"
+ );
+
+ {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ cluster.getStorageNodes().getConfig(builder);
+ cluster.getStorageNodes().getChildren().get("0").getConfig(builder);
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals("tcp/localhost:19106", config.persistence_provider().rpc().connectspec());
+ }
+
+ {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ cluster.getStorageNodes().getConfig(builder);
+ cluster.getStorageNodes().getChildren().get("1").getConfig(builder);
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals("tcp/localhost:19118", config.persistence_provider().rpc().connectspec());
+ }
+ }
+
+ @Test
+ public void testAlternativeNodeSyntax()
+ {
+ ContentCluster cluster = parse(
+ "<content version=\"1.0\" id=\"test\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " <nodes>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>\n" +
+ " </nodes>\n" +
+ "</content>"
+ );
+
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+
+ cluster.getConfig(builder);
+
+ StorDistributionConfig config = new StorDistributionConfig(builder);
+
+ assertEquals("invalid", config.group(0).name());
+ assertEquals("invalid", config.group(0).index());
+ assertEquals(2, config.group(0).nodes().size());
+ }
+
+ @Test
+ public void testReadyWhenInitialOne() {
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <redundancy>1</redundancy>\n" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>"
+ ).getConfig(builder);
+
+ StorDistributionConfig config = new StorDistributionConfig(builder);
+ assertEquals(1, config.initial_redundancy());
+ assertEquals(1, config.redundancy());
+ assertEquals(1, config.ready_copies());
+ }
+
+ public void testProvider(String tagName, StorServerConfig.Persistence_provider.Type.Enum type) {
+ ContentCluster cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <redundancy>3</redundancy>" +
+ " <engine>\n" +
+ " <" + tagName + "/>\n" +
+ " </engine>\n" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>"
+ );
+
+ {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ cluster.getStorageNodes().getConfig(builder);
+ cluster.getStorageNodes().getChildren().get("0").getConfig(builder);
+
+ StorServerConfig config = new StorServerConfig(builder);
+
+ assertEquals(type, config.persistence_provider().type());
+ }
+
+ {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ cluster.getDistributorNodes().getConfig(builder);
+ cluster.getDistributorNodes().getChildren().get("0").getConfig(builder);
+
+ StorServerConfig config = new StorServerConfig(builder);
+
+ assertEquals(type, config.persistence_provider().type());
+ }
+ }
+
+ @Test
+ public void testProviders() {
+ testProvider("proton", StorServerConfig.Persistence_provider.Type.RPC);
+ testProvider("rpc", StorServerConfig.Persistence_provider.Type.RPC);
+ testProvider("vds", StorServerConfig.Persistence_provider.Type.STORAGE);
+ testProvider("dummy", StorServerConfig.Persistence_provider.Type.DUMMY);
+ }
+
+ @Test
+ public void testMetrics() {
+ MetricsmanagerConfig.Builder builder = new MetricsmanagerConfig.Builder();
+
+ ContentCluster cluster = parse("<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</content>"
+ );
+ cluster.getConfig(builder);
+
+ MetricsmanagerConfig config = new MetricsmanagerConfig(builder);
+
+ assertEquals(6, config.consumer().size());
+ assertEquals("status", config.consumer(0).name());
+ assertEquals("*", config.consumer(0).addedmetrics(0));
+ assertEquals("partofsum", config.consumer(0).removedtags(0));
+
+ assertEquals("log", config.consumer(1).name());
+ assertEquals("logdefault", config.consumer(1).tags().get(0));
+ assertEquals("loadtype", config.consumer(1).removedtags(0));
+
+ assertEquals("yamas", config.consumer(2).name());
+ assertEquals("yamasdefault", config.consumer(2).tags().get(0));
+ assertEquals("loadtype", config.consumer(2).removedtags(0));
+
+ assertEquals("health", config.consumer(3).name());
+
+ assertEquals("statereporter", config.consumer(5).name());
+ assertEquals("*", config.consumer(5).addedmetrics(0));
+ assertEquals("thread", config.consumer(5).removedtags(0));
+ assertEquals("disk", config.consumer(5).tags(0));
+
+ cluster.getStorageNodes().getConfig(builder);
+ config = new MetricsmanagerConfig(builder);
+ assertEquals(6, config.consumer().size());
+
+ assertEquals("fleetcontroller", config.consumer(4).name());
+ assertEquals(9, config.consumer(4).addedmetrics().size());
+ assertEquals("vds.filestor.*.allthreads.put.sum", config.consumer(4).addedmetrics(0));
+ assertEquals("vds.filestor.*.allthreads.get.sum", config.consumer(4).addedmetrics(1));
+ assertEquals("vds.filestor.*.allthreads.multi.sum", config.consumer(4).addedmetrics(2));
+ assertEquals("vds.filestor.*.allthreads.update.sum", config.consumer(4).addedmetrics(3));
+ assertEquals("vds.filestor.*.allthreads.remove.sum", config.consumer(4).addedmetrics(4));
+ assertEquals("vds.filestor.*.allthreads.operations", config.consumer(4).addedmetrics(5));
+ assertEquals("vds.datastored.alldisks.docs", config.consumer(4).addedmetrics(6));
+ assertEquals("vds.datastored.alldisks.bytes", config.consumer(4).addedmetrics(7));
+ assertEquals("vds.datastored.alldisks.buckets", config.consumer(4).addedmetrics(8));
+ }
+
+ public MetricsmanagerConfig.Consumer getConsumer(String consumer, MetricsmanagerConfig config) {
+ for (MetricsmanagerConfig.Consumer c : config.consumer()) {
+ if (c.name().equals(consumer)) {
+ return c;
+ }
+ }
+
+ return null;
+ }
+
+ @Test
+ public void testConfiguredMetrics() throws Exception {
+ String xml = "" +
+ "<services>" +
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>" +
+ " <document type=\"type1\" mode='index'/>\n" +
+ " <document type=\"type2\" mode='index'/>\n" +
+ " </documents>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"node0\"/>\n" +
+ " </group>\n" +
+ "</content>" +
+ "<admin version=\"2.0\">" +
+ " <logserver hostalias=\"node0\"/>" +
+ " <adminserver hostalias=\"node0\"/>" +
+ " <metric-consumers>" +
+ " <consumer name=\"foobar\">" +
+ " <metric name=\"storage.foo.bar\"/>" +
+ " </consumer>" +
+ " <consumer name=\"log\">" +
+ " <metric name=\"extralogmetric\"/>" +
+ " <metric name=\"extralogmetric3\"/>" +
+ " </consumer>" +
+ " <consumer name=\"fleetcontroller\">" +
+ " <metric name=\"extraextra\"/>" +
+ " </consumer>" +
+ " </metric-consumers>" +
+ "</admin>" +
+ "</services>";
+
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1", "type2");
+ VespaModel model = new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
+
+ {
+ MetricsmanagerConfig.Builder builder = new MetricsmanagerConfig.Builder();
+ model.getConfig(builder, "storage/storage/0");
+ MetricsmanagerConfig config = new MetricsmanagerConfig(builder);
+
+ assertEquals("[storage.foo.bar]", getConsumer("foobar", config).addedmetrics().toString());
+ String expected =
+ "[extralogmetric\n" +
+ "extralogmetric3\n" +
+ "vds.filestor.alldisks.allthreads.put.sum\n" +
+ "vds.filestor.alldisks.allthreads.get.sum\n" +
+ "vds.filestor.alldisks.allthreads.remove.sum\n" +
+ "vds.filestor.alldisks.allthreads.update.sum\n" +
+ "vds.datastored.alldisks.docs\n" +
+ "vds.datastored.alldisks.bytes\n" +
+ "vds.filestor.alldisks.queuesize\n" +
+ "vds.filestor.alldisks.averagequeuewait.sum\n" +
+ "vds.visitor.cv_queuewaittime\n" +
+ "vds.visitor.allthreads.averagequeuewait\n" +
+ "vds.visitor.allthreads.averagevisitorlifetime\n" +
+ "vds.visitor.allthreads.created.sum]";
+ String actual = getConsumer("log", config).addedmetrics().toString().replaceAll(", ", "\n");
+ assertEquals(expected, actual);
+ assertEquals("[logdefault]", getConsumer("log", config).tags().toString());
+ expected =
+ "[extraextra\n" +
+ "vds.filestor.*.allthreads.put.sum\n" +
+ "vds.filestor.*.allthreads.get.sum\n" +
+ "vds.filestor.*.allthreads.multi.sum\n" +
+ "vds.filestor.*.allthreads.update.sum\n" +
+ "vds.filestor.*.allthreads.remove.sum\n" +
+ "vds.filestor.*.allthreads.operations\n" +
+ "vds.datastored.alldisks.docs\n" +
+ "vds.datastored.alldisks.bytes\n" +
+ "vds.datastored.alldisks.buckets]";
+ actual = getConsumer("fleetcontroller", config).addedmetrics().toString().replaceAll(", ", "\n");
+ assertEquals(expected, actual);
+ }
+
+ {
+ MetricsmanagerConfig.Builder builder = new MetricsmanagerConfig.Builder();
+ model.getConfig(builder, "storage/distributor/0");
+ MetricsmanagerConfig config = new MetricsmanagerConfig(builder);
+
+ assertEquals("[storage.foo.bar]", getConsumer("foobar", config).addedmetrics().toString());
+ assertEquals("[extralogmetric, extralogmetric3, vds.distributor.docsstored, vds.distributor.bytesstored, vds.idealstate.delete_bucket.done_ok, vds.idealstate.merge_bucket.done_ok, vds.idealstate.split_bucket.done_ok, vds.idealstate.join_bucket.done_ok, vds.idealstate.buckets_rechecking]", getConsumer("log", config).addedmetrics().toString());
+ assertEquals("[logdefault]", getConsumer("log", config).tags().toString());
+ assertEquals("[extraextra]", getConsumer("fleetcontroller", config).addedmetrics().toString());
+ }
+ }
+
+ @Test
+ public void requireThatPreShutdownCommandIsSet() {
+ ContentCluster cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">" +
+ " <documents/>" +
+ " <engine>" +
+ " <proton>" +
+ " <flush-on-shutdown>true</flush-on-shutdown>" +
+ " </proton>" +
+ " </engine>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>");
+ assertThat(cluster.getSearch().getSearchNodes().size(), is(1));
+ assertTrue(cluster.getSearch().getSearchNodes().get(0).getPreShutdownCommand().isPresent());
+
+ cluster = parse(
+ "<content version=\"1.0\" id=\"storage\">" +
+ " <documents/>" +
+ " <engine>" +
+ " <proton>" +
+ " <flush-on-shutdown> \n " +
+ " true </flush-on-shutdown>" +
+ " </proton>" +
+ " </engine>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>");
+ assertThat(cluster.getSearch().getSearchNodes().size(), is(1));
+ assertTrue(cluster.getSearch().getSearchNodes().get(0).getPreShutdownCommand().isPresent());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/ContentBaseTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentBaseTest.java
new file mode 100644
index 00000000000..038179af9cf
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentBaseTest.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+public class ContentBaseTest {
+ public static String getHosts() {
+ return "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<hosts> " +
+ " <host name='foo'>" +
+ " <alias>node0</alias>" +
+ " </host>" +
+ "</hosts>";
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java
new file mode 100644
index 00000000000..d866fd225b3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchClusterTest.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.utils.ContentClusterBuilder;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import junit.framework.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.yahoo.vespa.model.content.utils.ContentClusterUtils.createCluster;
+import static junit.framework.TestCase.assertEquals;
+
+/**
+ * Unit tests for content search cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class ContentSearchClusterTest {
+
+ private static double EPSILON = 0.000001;
+
+ private static ContentCluster createClusterWithOneDocumentType() throws Exception {
+ return createCluster(new ContentClusterBuilder().getXml());
+ }
+
+ private static ContentCluster createClusterWithTwoDocumentType() throws Exception {
+ List<String> docTypes = Arrays.asList("foo", "bar");
+ return createCluster(new ContentClusterBuilder().docTypes(docTypes).getXml(),
+ ApplicationPackageUtils.generateSearchDefinitions(docTypes));
+ }
+
+ private static ProtonConfig getProtonConfig(ContentCluster cluster) {
+ ProtonConfig.Builder protonCfgBuilder = new ProtonConfig.Builder();
+ cluster.getSearch().getConfig(protonCfgBuilder);
+ return new ProtonConfig(protonCfgBuilder);
+ }
+
+ @Test
+ public void requireThatProtonInitializeThreadsIsSet() throws Exception {
+ assertEquals(2, getProtonConfig(createClusterWithOneDocumentType()).initialize().threads());
+ assertEquals(3, getProtonConfig(createClusterWithTwoDocumentType()).initialize().threads());
+ }
+
+ @Test
+ public void requireThatProtonResourceLimitsCanBeSet() throws Exception {
+ String clusterXml = new ContentClusterBuilder().protonDiskLimit(0.88).protonMemoryLimit(0.77).getXml();
+ ProtonConfig cfg = getProtonConfig(createCluster(clusterXml));
+ assertEquals(0.88, cfg.writefilter().disklimit(), EPSILON);
+ assertEquals(0.77, cfg.writefilter().memorylimit(), EPSILON);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchTest.java
new file mode 100644
index 00000000000..5d3a1105289
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentSearchTest.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ContentSearchTest {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ ContentSearch search = new ContentSearch.Builder()
+ .setQueryTimeout(1.0)
+ .setVisibilityDelay(2.0)
+ .build();
+ assertEquals(1.0, search.getQueryTimeout(), 1E-6);
+ assertEquals(2.0, search.getVisibilityDelay(), 1E-6);
+ }
+
+ @Test
+ public void requireThatDefaultsAreNull() {
+ ContentSearch search = new ContentSearch.Builder().build();
+ assertNull(search.getQueryTimeout());
+ assertNull(search.getVisibilityDelay());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/DistributorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/DistributorTest.java
new file mode 100644
index 00000000000..d886b0feee3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/DistributorTest.java
@@ -0,0 +1,374 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.core.StorCommunicationmanagerConfig;
+import com.yahoo.vespa.config.content.core.StorDistributormanagerConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.utils.ContentClusterUtils;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.*;
+/**
+ * Test for content DistributorCluster.
+ */
+public class DistributorTest {
+
+ ContentCluster parseCluster(String xml) {
+ try {
+ final List<String> searchDefs = ApplicationPackageUtils.generateSearchDefinitions("music", "movies", "bunnies");
+ MockRoot root = ContentClusterUtils.createMockRoot(searchDefs);
+ return ContentClusterUtils.createCluster(xml, root);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ DistributorCluster parse(String xml) {
+ return parseCluster(xml).getDistributorNodes();
+ }
+
+ @Test
+ public void testBasics() {
+
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ parse("<content id=\"foofighters\"><documents/>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>\n").
+ getConfig(builder);
+
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals(true, config.is_distributor());
+ assertEquals("foofighters", config.cluster_name());
+ }
+
+ @Test
+ public void testRevertDefaultOffForSearch() {
+ {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(true, conf.enable_revert());
+ }
+ {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(false, conf.enable_revert());
+ }
+ }
+
+ @Test
+ public void testSplitAndJoin() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " <tuning>\n" +
+ " <bucket-splitting max-documents=\"2K\" max-size=\"25M\" minimum-bits=\"8\" />\n" +
+ " </tuning>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+
+ assertEquals(2048, conf.splitcount());
+ assertEquals(1024, conf.joincount());
+ assertEquals(26214400, conf.splitsize());
+ assertEquals(13107200, conf.joinsize());
+ assertEquals(8, conf.minsplitcount());
+ assertEquals(true, conf.inlinebucketsplitting());
+ }
+
+ @Test
+ public void testThatGroupsAreCountedInWhenComputingSplitBits() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ ContentCluster cluster = parseCluster("<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " <tuning>" +
+ " <distribution type=\"legacy\"/>" +
+ " </tuning>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>");
+ cluster.getConfig(builder);
+
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+
+ assertEquals(1024, conf.splitcount());
+ assertEquals(512, conf.joincount());
+ assertEquals(33544432, conf.splitsize());
+ assertEquals(16000000, conf.joinsize());
+ assertEquals(8, conf.minsplitcount());
+ assertEquals(true, conf.inlinebucketsplitting());
+
+ cluster = parseCluster("<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " <tuning>" +
+ " <distribution type=\"legacy\"/>" +
+ " </tuning>\n" +
+ " <group>" +
+ " <distribution partitions=\"1|*\"/>" +
+ " <group name=\"a\" distribution-key=\"0\">" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ " <group name=\"b\" distribution-key=\"1\">" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ " </group>" +
+ "</cluster>");
+ cluster.getConfig(builder);
+
+ conf = new StorDistributormanagerConfig(builder);
+
+ assertEquals(1024, conf.splitcount());
+ assertEquals(512, conf.joincount());
+ assertEquals(33544432, conf.splitsize());
+ assertEquals(16000000, conf.joinsize());
+ assertEquals(1, conf.minsplitcount());
+ assertEquals(true, conf.inlinebucketsplitting());
+ }
+
+ @Test
+ public void testMaxMergesPerNode() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ DistributorCluster dcluster = parse("<content id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>");
+ ((ContentCluster) dcluster.getParent()).getConfig(builder);
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(16, conf.maximum_nodes_per_merge());
+
+ builder = new StorDistributormanagerConfig.Builder();
+ dcluster = parse("<content id=\"storage\">\n" +
+ " <documents/>" +
+ " <tuning>\n" +
+ " <merges max-nodes-per-merge=\"4\"/>\n" +
+ " </tuning>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>");
+ ((ContentCluster) dcluster.getParent()).getConfig(builder);
+ conf = new StorDistributormanagerConfig(builder);
+ assertEquals(4, conf.maximum_nodes_per_merge());
+ }
+
+ @Test
+ public void testGarbageCollectionSetExplicitly() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents garbage-collection=\"true\">\n" +
+ " <document type=\"music\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(3600, conf.garbagecollection().interval());
+ assertEquals("not ((music))", conf.garbagecollection().selectiontoremove());
+ }
+
+ @Test
+ public void testGarbageCollectionInterval() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents garbage-collection=\"true\" garbage-collection-interval=\"30\">\n" +
+ " <document type=\"music\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(30, conf.garbagecollection().interval());
+ }
+
+ @Test
+ public void testGarbageCollectionOffByDefault() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents>\n" +
+ " <document type=\"music\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(0, conf.garbagecollection().interval());
+ assertEquals("", conf.garbagecollection().selectiontoremove());
+ }
+
+ @Test
+ public void testComplexGarbageCollectionSelectionForIndexedSearch() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"foo\">\n" +
+ " <documents garbage-collection=\"true\" selection=\"true\">" +
+ " <document type=\"music\" selection=\"music.year &lt; now()\"/>\n" +
+ " <document type=\"movies\" selection=\"movies.year &lt; now() - 1200\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(3600, conf.garbagecollection().interval());
+ assertEquals(
+ "not ((true) and ((music and (music.year < now())) or (movies and (movies.year < now() - 1200))))",
+ conf.garbagecollection().selectiontoremove());
+ }
+
+ @Test
+ public void testGarbageCollectionDisabledIfForced() {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse("<cluster id=\"foo\">\n" +
+ " <documents selection=\"true\" garbage-collection=\"false\" garbage-collection-interval=\"30\">\n" +
+ " <document type=\"music\" selection=\"music.year &lt; now()\"/>\n" +
+ " <document type=\"movies\" selection=\"movies.year &lt; now() - 1200\"/>\n" +
+ " </documents>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>").getConfig(builder);
+
+ StorDistributormanagerConfig conf = new StorDistributormanagerConfig(builder);
+ assertEquals(0, conf.garbagecollection().interval());
+ assertEquals("", conf.garbagecollection().selectiontoremove());
+ }
+
+ @Test
+ public void testPortOverride() {
+ StorCommunicationmanagerConfig.Builder builder = new StorCommunicationmanagerConfig.Builder();
+ DistributorCluster cluster =
+ parse("<cluster id=\"storage\" distributor-base-port=\"14065\">" +
+ " <documents/>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>");
+
+ cluster.getChildren().get("0").getConfig(builder);
+ StorCommunicationmanagerConfig config = new StorCommunicationmanagerConfig(builder);
+ assertEquals(14066, config.rpcport());
+ }
+
+ private StorDistributormanagerConfig clusterXmlToConfig(String xml) {
+ StorDistributormanagerConfig.Builder builder = new StorDistributormanagerConfig.Builder();
+ parse(xml).getConfig(builder);
+ return new StorDistributormanagerConfig(builder);
+ }
+
+ private static class DocDef {
+ public final String type;
+ public final String mode;
+
+ private DocDef(String type, String mode) {
+ this.type = type;
+ this.mode = mode;
+ }
+
+ public static DocDef storeOnly(String type) {
+ return new DocDef(type, "store-only");
+ }
+
+ public static DocDef index(String type) {
+ return new DocDef(type, "index");
+ }
+
+ public static DocDef streaming(String type) {
+ return new DocDef(type, "streaming");
+ }
+ }
+
+ private String generateXmlForDocDefs(DocDef... defs) {
+ return "<content id='storage'>\n" +
+ " <documents>\n" +
+ Arrays.stream(defs)
+ .map(def -> String.format(" <document type='%s' mode='%s'/>", def.type, def.mode))
+ .collect(Collectors.joining("\n")) +
+ "\n </documents>\n" +
+ "</content>";
+ }
+
+ @Test
+ public void bucket_activation_disabled_if_no_documents_in_indexed_mode() {
+ StorDistributormanagerConfig config = clusterXmlToConfig(
+ generateXmlForDocDefs(DocDef.storeOnly("music")));
+ assertThat(config.disable_bucket_activation(), is(true));
+ }
+
+ @Test
+ public void bucket_activation_enabled_with_single_indexed_document() {
+ StorDistributormanagerConfig config = clusterXmlToConfig(
+ generateXmlForDocDefs(DocDef.index("music")));
+ assertThat(config.disable_bucket_activation(), is(false));
+ }
+
+ @Test
+ public void bucket_activation_enabled_with_multiple_indexed_documents() {
+ StorDistributormanagerConfig config = clusterXmlToConfig(
+ generateXmlForDocDefs(DocDef.index("music"),
+ DocDef.index("movies")));
+ assertThat(config.disable_bucket_activation(), is(false));
+ }
+
+ @Test
+ public void bucket_activation_enabled_if_at_least_one_document_indexed() {
+ StorDistributormanagerConfig config = clusterXmlToConfig(
+ generateXmlForDocDefs(DocDef.storeOnly("music"),
+ DocDef.streaming("bunnies"),
+ DocDef.index("movies")));
+ assertThat(config.disable_bucket_activation(), is(false));
+ }
+
+ @Test
+ public void bucket_activation_disabled_for_single_streaming_type() {
+ StorDistributormanagerConfig config = clusterXmlToConfig(
+ generateXmlForDocDefs(DocDef.streaming("music")));
+ assertThat(config.disable_bucket_activation(), is(true));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java
new file mode 100644
index 00000000000..204491b0724
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/FleetControllerClusterTest.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Created with IntelliJ IDEA.
+ * User: thomasg
+ * Date: 5/10/12
+ * Time: 2:29 PM
+ * To change this template use File | Settings | File Templates.
+ */
+public class FleetControllerClusterTest {
+ ClusterControllerConfig parse(String xml) {
+ Document doc = XML.getDocument(xml);
+ return new ClusterControllerConfig.Builder("storage", new ModelElement(doc.getDocumentElement())).build(new MockRoot(),
+ new ModelElement(doc.getDocumentElement()).getXml());
+ }
+
+ @Test
+ public void testParameters() {
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <tuning>\n" +
+ " <bucket-splitting minimum-bits=\"7\" />" +
+ " <cluster-controller>\n" +
+ " <init-progress-time>13</init-progress-time>\n" +
+ " <transition-time>27</transition-time>\n" +
+ " <max-premature-crashes>4</max-premature-crashes>\n" +
+ " <stable-state-period>72</stable-state-period>\n" +
+ " <min-distributor-up-ratio>0.7</min-distributor-up-ratio>\n" +
+ " <min-storage-up-ratio>0.3</min-storage-up-ratio>\n" +
+ " </cluster-controller>\n" +
+ " </tuning>\n" +
+ "</cluster>").
+ getConfig(builder);
+
+ FleetcontrollerConfig config = new FleetcontrollerConfig(builder);
+ assertEquals(13 * 1000, config.init_progress_time());
+ assertEquals(27 * 1000, config.storage_transition_time());
+ assertEquals(4, config.max_premature_crashes());
+ assertEquals(72 * 1000, config.stable_state_time_period());
+ assertEquals(0.7, config.min_distributor_up_ratio(), 0.01);
+ assertEquals(0.3, config.min_storage_up_ratio(), 0.01);
+ assertEquals(7, config.ideal_distribution_bits());
+ }
+
+ @Test
+ public void testDurationParameters() {
+ FleetcontrollerConfig.Builder builder = new FleetcontrollerConfig.Builder();
+ parse("<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <tuning>\n" +
+ " <cluster-controller>\n" +
+ " <init-progress-time>13ms</init-progress-time>\n" +
+ " </cluster-controller>\n" +
+ " </tuning>\n" +
+ "</cluster>").
+ getConfig(builder);
+
+ FleetcontrollerConfig config = new FleetcontrollerConfig(builder);
+ assertEquals(13, config.init_progress_time());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/GenericConfigTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/GenericConfigTest.java
new file mode 100644
index 00000000000..ba408bfbddf
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/GenericConfigTest.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.storage.StorMemfilepersistenceConfig;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.storagecluster.StorageCluster;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ * @since 5.1.8
+ */
+public class GenericConfigTest {
+
+ private VespaModel model;
+
+ private String servicesXml() {
+ return "" +
+ "<services version='1.0'>" +
+ " <config name='vespa.config.storage.stor-memfilepersistence'>" +
+ " <disk_full_factor>0.001</disk_full_factor> " +
+ " </config>" +
+ " <admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node0\" />" +
+ " <cluster-controllers>" +
+ " <cluster-controller hostalias='node0'/>" +
+ " </cluster-controllers>" +
+ " </admin>" +
+ " <content version='1.0' id='storage'>" +
+ " <documents>" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " </documents>" +
+ " <config name='config.juniperrc'>" +
+ " <length>1024</length>" +
+ " </config>" +
+ " <redundancy>1</redundancy>" +
+ " <group>" +
+ " <node distribution-key='0' hostalias='node0'/>" +
+ " </group>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " </content>" +
+ "</services>";
+ }
+
+ @Before
+ public void getVespaModel() throws IOException, SAXException, ParseException {
+ model = (new VespaModelCreatorWithMockPkg(ContentBaseTest.getHosts(), servicesXml(), ApplicationPackageUtils.generateSearchDefinitions("type1"))).create();
+ }
+
+ @Test
+ public void config_override_on_root_is_visible_on_storage_cluster() throws Exception {
+ StorageCluster cluster = model.getContentClusters().get("storage").getStorageNodes();
+
+ StorMemfilepersistenceConfig config = model.getConfig(StorMemfilepersistenceConfig.class, cluster.getConfigId());
+ assertThat(config.disk_full_factor(), is(0.001));
+ }
+
+ @Test
+ public void config_override_on_root_is_visible_on_content_cluster() throws Exception {
+ ContentCluster cluster = model.getContentClusters().get("storage");
+
+ StorMemfilepersistenceConfig config = model.getConfig(StorMemfilepersistenceConfig.class, cluster.getConfigId());
+ assertThat(config.disk_full_factor(), is(0.001));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java
new file mode 100644
index 00000000000..e9da8dd2376
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedHierarchicDistributionTest.java
@@ -0,0 +1,298 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.SimpleConfigProducer;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.search.DispatchGroup;
+import com.yahoo.vespa.model.search.SearchInterface;
+import com.yahoo.vespa.model.search.SearchNode;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static com.yahoo.vespa.model.content.utils.ContentClusterUtils.createCluster;
+import static com.yahoo.vespa.model.content.utils.ContentClusterUtils.createClusterXml;
+import static com.yahoo.vespa.model.search.utils.DispatchUtils.assertEngine;
+import static com.yahoo.vespa.model.search.utils.DispatchUtils.getDataset;
+
+
+/**
+ * Unit tests for hierarchic distribution in an indexed content cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class IndexedHierarchicDistributionTest {
+
+ private ContentCluster addDispatcher(ContentCluster c) {
+ c.getSearch().getIndexed().addTld(new SimpleConfigProducer(new MockRoot(""), ""), new HostResource(new Host(new MockRoot(""), "mockhost")));
+ return c;
+ }
+
+ private ContentCluster getOneGroupCluster() throws Exception {
+ String groupXml = " <group>\n" +
+ " <node distribution-key='0' hostalias='mockhost'/>\n" +
+ " <node distribution-key='1' hostalias='mockhost'/>\n" +
+ " <node distribution-key='2' hostalias='mockhost'/>\n" +
+ " </group>\n";
+ return addDispatcher(createCluster(createClusterXml(groupXml, 2, 2)));
+ }
+
+ private String getTwoGroupsXml(String partitions) {
+ return " <group>\n" +
+ " <distribution partitions='" + partitions + "'/>\n" +
+ " <group distribution-key='0' name='group0'>\n" +
+ " <node distribution-key='0' hostalias='mockhost'/>\n" +
+ " <node distribution-key='1' hostalias='mockhost'/>\n" +
+ " <node distribution-key='2' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " <group distribution-key='1' name='group1'>\n" +
+ " <node distribution-key='3' hostalias='mockhost'/>\n" +
+ " <node distribution-key='4' hostalias='mockhost'/>\n" +
+ " <node distribution-key='5' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " </group>\n";
+ }
+
+ private ContentCluster getTwoGroupsCluster() throws Exception {
+ return addDispatcher(createCluster(createClusterXml(getTwoGroupsXml("3|*"), 6, 6)));
+ }
+
+ private ContentCluster getTwoGroupsCluster(int redundancy, int searchableCopies, String partitions) throws Exception {
+ return addDispatcher(createCluster(createClusterXml(getTwoGroupsXml(partitions), redundancy, searchableCopies)));
+ }
+
+ private void assertSearchNode(int expRowId, int expPartitionId, int expDistibutionKey, SearchNode node) {
+ assertEquals(expRowId, node.getNodeSpec().rowId());
+ assertEquals(expPartitionId, node.getNodeSpec().partitionId());
+ assertEquals(expDistibutionKey, ((ContentNode)node.getServiceLayerService()).getDistributionKey());
+ }
+
+ private StorDistributionConfig getStorDistributionConfig(ContentCluster c) {
+ StorDistributionConfig.Builder b = new StorDistributionConfig.Builder();
+ c.getConfig(b);
+ return new StorDistributionConfig(b);
+ }
+
+ @Test
+ public void requireThatSearchNodesAreCorrectWithOneGroup() throws Exception {
+ ContentCluster c = getOneGroupCluster();
+ List<SearchNode> searchNodes = c.getSearch().getSearchNodes();
+
+ assertEquals(3, searchNodes.size());
+ assertSearchNode(0, 0, 0, searchNodes.get(0));
+ assertSearchNode(0, 1, 1, searchNodes.get(1));
+ assertSearchNode(0, 2, 2, searchNodes.get(2));
+ }
+
+ @Test
+ public void requireThatDispatcherIsCorrectWithOneGroup() throws Exception {
+ ContentCluster c = getOneGroupCluster();
+ PartitionsConfig.Dataset dataset = getDataset(c.getSearch().getIndexed().getTLDs().get(0));
+
+ assertEquals(3, dataset.numparts());
+ assertEquals(PartitionsConfig.Dataset.Querydistribution.AUTOMATIC, dataset.querydistribution());
+ List<PartitionsConfig.Dataset.Engine> engines = dataset.engine();
+ assertEquals(3, engines.size());
+ assertEngine(0, 0, engines.get(0));
+ assertEngine(0, 1, engines.get(1));
+ assertEngine(0, 2, engines.get(2));
+ }
+
+ @Test
+ public void requireThatActivePerLeafGroupIsDefaultWithOneGroup() throws Exception {
+ ContentCluster c = getOneGroupCluster();
+ assertFalse(getStorDistributionConfig(c).active_per_leaf_group());
+ }
+
+ @Test
+ public void requireThatSearchNodesAreCorrectWithTwoGroups() throws Exception {
+ ContentCluster c = getTwoGroupsCluster();
+ List<SearchNode> searchNodes = c.getSearch().getSearchNodes();
+
+ assertEquals(6, searchNodes.size());
+ assertSearchNode(0, 0, 0, searchNodes.get(0));
+ assertSearchNode(0, 1, 1, searchNodes.get(1));
+ assertSearchNode(0, 2, 2, searchNodes.get(2));
+ assertSearchNode(1, 0, 3, searchNodes.get(3));
+ assertSearchNode(1, 1, 4, searchNodes.get(4));
+ assertSearchNode(1, 2, 5, searchNodes.get(5));
+ }
+
+ @Test
+ public void requireThatDispatcherIsCorrectWithTwoGroups() throws Exception {
+ ContentCluster c = getTwoGroupsCluster();
+ PartitionsConfig.Dataset dataset = getDataset(c.getSearch().getIndexed().getTLDs().get(0));
+
+ assertEquals(3, dataset.numparts());
+ assertEquals(2, dataset.maxnodesdownperfixedrow());
+ assertEquals(PartitionsConfig.Dataset.Querydistribution.FIXEDROW, dataset.querydistribution());
+ List<PartitionsConfig.Dataset.Engine> engines = dataset.engine();
+ assertEquals(6, engines.size());
+ assertEngine(0, 0, engines.get(0));
+ assertEngine(1, 0, engines.get(1));
+ assertEngine(0, 1, engines.get(2));
+ assertEngine(1, 1, engines.get(3));
+ assertEngine(0, 2, engines.get(4));
+ assertEngine(1, 2, engines.get(5));
+ }
+
+ @Test
+ public void requireThatActivePerLeafGroupIsSetWithTwoGroups() throws Exception {
+ ContentCluster c = getTwoGroupsCluster();
+ assertTrue(getStorDistributionConfig(c).active_per_leaf_group());
+ }
+
+ private ContentCluster getIllegalMultipleGroupsLevelCluster() throws Exception {
+ String groupXml = " <group>\n" +
+ " <distribution partitions='2|*'/>\n" +
+ " <group distribution-key='0' name='group0'>\n" +
+ " <distribution partitions='1|*'/>\n" +
+ " <group distribution-key='0' name='group00'>\n" +
+ " <node distribution-key='0' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " <group distribution-key='1' name='group01'>\n" +
+ " <node distribution-key='1' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ " </group>\n";
+ return createCluster(createClusterXml(groupXml, 2, 2));
+ }
+
+ private String getOddGroupsClusterXml() throws Exception {
+ return " <group>\n" +
+ " <distribution partitions='2|*'/>\n" +
+ " <group distribution-key='0' name='group0'>\n" +
+ " <node distribution-key='0' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " <group distribution-key='1' name='group1'>\n" +
+ " <node distribution-key='1' hostalias='mockhost'/>\n" +
+ " <node distribution-key='2' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " </group>\n";
+ }
+ private ContentCluster getIllegalGroupsCluster() throws Exception {
+ return createCluster(createClusterXml(getOddGroupsClusterXml(), 4, 4));
+ }
+
+ private String getRandomDispatchXml() {
+ return "<tuning>" +
+ " <dispatch>" +
+ " <dispatch-policy>random</dispatch-policy>" +
+ " </dispatch>" +
+ "</tuning>";
+ }
+
+ private ContentCluster getOddGroupsCluster() throws Exception {
+ String groupXml = " <group>\n" +
+ " <distribution partitions='2|*'/>\n" +
+ " <group distribution-key='0' name='group0'>\n" +
+ " <node distribution-key='0' hostalias='mockhost'/>\n" +
+ " <node distribution-key='1' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " <group distribution-key='1' name='group1'>\n" +
+ " <node distribution-key='3' hostalias='mockhost'/>\n" +
+ " <node distribution-key='4' hostalias='mockhost'/>\n" +
+ " <node distribution-key='5' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " </group>\n";
+ return createCluster(createClusterXml(groupXml, Optional.of(getRandomDispatchXml()), 4, 4));
+ }
+
+ @Test
+ public void requireThatWeMustHaveOnlyOneGroupLevel() {
+ try {
+ getIllegalMultipleGroupsLevelCluster();
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("sub group 'group0' contains 2 sub groups."));
+ }
+ }
+
+ @Test
+ public void requireThatLeafGroupsMustHaveEqualNumberOfNodes() {
+ try {
+ getIllegalGroupsCluster();
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("leaf group 'group0' contains 1 node(s) while leaf group 'group1' contains 2 node(s)"));
+ }
+ }
+
+ @Test
+ public void requireThatLeafGroupsCanHaveUnequalNumberOfNodesIfRandomPolicy() throws Exception {
+ ContentCluster c = getOddGroupsCluster();
+ DispatchGroup dg = c.getSearch().getIndexed().getRootDispatch();
+ assertEquals(8, dg.getRowBits());
+ assertEquals(3, dg.getNumPartitions());
+ assertEquals(true, dg.useFixedRowInDispatch());
+ assertEquals(1, dg.getMaxNodesDownPerFixedRow());
+ ArrayList<SearchInterface> list = new ArrayList<>();
+ for(SearchInterface si : dg.getSearchersIterable()) {
+ list.add(si);
+ }
+ assertEquals(5, list.size());
+ assertEquals(0, list.get(0).getNodeSpec().partitionId());
+ assertEquals(0, list.get(0).getNodeSpec().rowId());
+ assertEquals(0, list.get(1).getNodeSpec().partitionId());
+ assertEquals(1, list.get(1).getNodeSpec().rowId());
+ assertEquals(1, list.get(2).getNodeSpec().partitionId());
+ assertEquals(0, list.get(2).getNodeSpec().rowId());
+ assertEquals(1, list.get(3).getNodeSpec().partitionId());
+ assertEquals(1, list.get(3).getNodeSpec().rowId());
+ assertEquals(2, list.get(4).getNodeSpec().partitionId());
+ assertEquals(1, list.get(4).getNodeSpec().rowId());
+ }
+
+ @Test
+ public void requireThatLeafGroupsCountMustBeAFactorOfRedundancy() {
+ try {
+ getTwoGroupsCluster(3, 3, "2|*");
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Expected number of leaf groups (2) to be a factor of redundancy (3)"));
+ }
+ }
+
+ @Test
+ public void requireThatRedundancyPerGroupMustBeIsEqual() {
+ try {
+ getTwoGroupsCluster(4, 4, "1|*");
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Expected distribution partitions should be '2|*'"));
+ }
+ }
+
+ @Test
+ public void requireThatReadyCopiesMustBeEqualToRedundancy() {
+ try {
+ getTwoGroupsCluster(4, 3, "2|*");
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("Expected equal amount of ready copies per group"));
+ }
+ }
+
+ @Test
+ public void allowLessReadyCopiesThanRedundancy() throws Exception {
+ getTwoGroupsCluster(4, 2, "2|*");
+ }
+
+ @Test
+ public void allowNoReadyCopies() throws Exception {
+ // The active one should be indexed anyhow. Setting up no ready copies
+ getTwoGroupsCluster(4, 0, "2|*");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedSearchNodeNamingTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedSearchNodeNamingTest.java
new file mode 100644
index 00000000000..8593d4f01b5
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedSearchNodeNamingTest.java
@@ -0,0 +1,96 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.search.SearchNode;
+import org.junit.Test;
+
+import java.util.List;
+
+import static com.yahoo.vespa.model.content.utils.ContentClusterUtils.createCluster;
+import static com.yahoo.vespa.model.content.utils.ContentClusterUtils.createClusterXml;
+import static junit.framework.TestCase.assertEquals;
+
+/**
+ * Unit tests for the naming of search nodes base dir and config ids in an indexed content cluster.
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class IndexedSearchNodeNamingTest {
+
+ private ContentCluster getSingleNodeCluster() throws Exception {
+ String groupXml = " <group>\n" +
+ " <node distribution-key='3' hostalias='mockhost'/>\n" +
+ " </group>\n";
+ return createCluster(createClusterXml(groupXml, 1, 1));
+ }
+
+ private ContentCluster getMultiNodeCluster() throws Exception {
+ String groupXml = " <group>\n" +
+ " <node distribution-key='5' hostalias='mockhost'/>\n" +
+ " <node distribution-key='3' hostalias='mockhost'/>\n" +
+ " <node distribution-key='7' hostalias='mockhost'/>\n" +
+ " </group>\n";
+ return createCluster(createClusterXml(groupXml, 1, 1));
+ }
+
+ private ContentCluster getMultiGroupCluster() throws Exception {
+ String groupXml = " <group>\n" +
+ " <distribution partitions='1|*'/>\n" +
+ " <group distribution-key='3' name='group0'>\n" +
+ " <node distribution-key='7' hostalias='mockhost'/>\n" +
+ " <node distribution-key='11' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " <group distribution-key='5' name='group1'>\n" +
+ " <node distribution-key='17' hostalias='mockhost'/>\n" +
+ " <node distribution-key='13' hostalias='mockhost'/>\n" +
+ " </group>\n" +
+ " </group>\n";
+ return createCluster(createClusterXml(groupXml, 2, 2));
+ }
+
+ private void assertBaseDir(String expected, SearchNode node) {
+ ProtonConfig.Builder builder = new ProtonConfig.Builder();
+ node.getConfig(builder);
+ ProtonConfig cfg = new ProtonConfig(builder);
+ assertEquals(expected, cfg.basedir());
+ }
+
+ private void assertConfigId(String expected, SearchNode node) {
+ assertEquals(expected, node.getConfigId());
+ }
+
+ private void assertSearchNode(String expName, String expId, SearchNode node) {
+ assertBaseDir(Defaults.getDefaults().vespaHome() + "var/db/vespa/search/cluster.mycluster/" + expName, node);
+ assertConfigId("mycluster/search/cluster.mycluster/" + expId, node);
+ }
+
+ @Test
+ public void requireThatSingleNodeIsNamedAfterDistributionKey() throws Exception {
+ ContentCluster cluster = getSingleNodeCluster();
+ List<SearchNode> nodes = cluster.getSearch().getSearchNodes();
+ assertSearchNode("n3", "3", nodes.get(0));
+ }
+
+ @Test
+ public void requireThatMultipleNodesAreNamedAfterDistributionKey() throws Exception {
+ ContentCluster cluster = getMultiNodeCluster();
+ List<SearchNode> nodes = cluster.getSearch().getSearchNodes();
+ assertEquals(3, nodes.size());
+ assertSearchNode("n5", "5", nodes.get(0));
+ assertSearchNode("n3", "3", nodes.get(1));
+ assertSearchNode("n7", "7", nodes.get(2));
+ }
+
+ @Test
+ public void requireThatNodesInHierarchicGroupsAreNamedAfterDistributionKey() throws Exception {
+ ContentCluster cluster = getMultiGroupCluster();
+ List<SearchNode> nodes = cluster.getSearch().getSearchNodes();
+ assertEquals(4, nodes.size());
+ assertSearchNode("n7", "7", nodes.get(0));
+ assertSearchNode("n11", "11", nodes.get(1));
+ assertSearchNode("n17", "17", nodes.get(2));
+ assertSearchNode("n13", "13", nodes.get(3));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedTest.java
new file mode 100644
index 00000000000..0f62bc4760d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexedTest.java
@@ -0,0 +1,292 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.cloud.config.ClusterListConfig;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.messagebus.routing.RouteSpec;
+import com.yahoo.messagebus.routing.RoutingTableSpec;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.configdefinition.SpecialtokensConfig;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.routing.DocumentProtocol;
+import com.yahoo.vespa.model.routing.Routing;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+/**
+ * Test for using the content model to create indexed search clusters.
+ */
+public class IndexedTest extends ContentBaseTest {
+ private String createVespaServices(String pre, List<String> sdNames, String post, String mode) {
+ StringBuilder retval = new StringBuilder();
+ retval.append(pre);
+
+
+ for (String sdName : sdNames) {
+ retval.append("<document type='" + sdName + "' " + "mode='" + mode + "'/>");
+ }
+
+ retval.append(post);
+ return retval.toString();
+ }
+ private String createProtonIndexedVespaServices(List<String> sdNames) {
+ String pre = "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0'/>" +
+ " </admin>" +
+ " <config name='vespa.configdefinition.specialtokens'>" +
+ " <tokenlist operation='append'>" +
+ " <name>default</name>" +
+ " <tokens operation='append'>" +
+ " <token>dvd+-r</token>" +
+ " </tokens>" +
+ " </tokenlist>" +
+ " </config>" +
+ " <jdisc version='1.0'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='node0'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content version='1.0' id='test'>" +
+ " <redundancy>1</redundancy>" +
+ " <engine>" +
+ " <proton>" +
+ " <visibility-delay>34</visibility-delay>" +
+ " </proton>" +
+ " </engine>" +
+ " <documents>";
+
+ String post = " </documents>" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='3' />" +
+ " </group>" +
+ "</content>" +
+ "</services>";
+ return createVespaServices(pre, sdNames, post, "index");
+ }
+ private String createProtonStreamingVespaServices(List<String> sdNames) {
+ String pre = "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0'/>" +
+ " </admin>" +
+ " <jdisc version='1.0'>" +
+ " <search/>" +
+ " <nodes>" +
+ " <node hostalias='node0'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content version='1.0' id='test'>" +
+ " <redundancy>1</redundancy>\n" +
+ " <engine>" +
+ " <proton/>" +
+ " </engine>" +
+ " <documents>";
+ String post =
+ " </documents>" +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='3' />" +
+ " </group>" +
+ "</content>" +
+ "</services>";
+ return createVespaServices(pre, sdNames, post, "streaming");
+ }
+
+ private VespaModel getIndexedVespaModel() throws ParseException, IOException, SAXException {
+ return getIndexedVespaModelCreator().create();
+ }
+
+ private VespaModelCreatorWithMockPkg getIndexedVespaModelCreator() throws ParseException, IOException, SAXException {
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1", "type2", "type3");
+ return new VespaModelCreatorWithMockPkg(getHosts(), createProtonIndexedVespaServices(Arrays.asList("type1", "type2", "type3")), sds);
+ }
+
+ private VespaModel getStreamingVespaModel() throws ParseException, IOException, SAXException {
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1");
+ return new VespaModelCreatorWithMockPkg(getHosts(), createProtonStreamingVespaServices(Arrays.asList("type1")), sds).create();
+ }
+
+ @Test
+ public void requireMultipleDocumentTypes() throws ParseException, IOException, SAXException {
+ VespaModelCreatorWithMockPkg creator = getIndexedVespaModelCreator();
+ VespaModel model = creator.create();
+ DeployState deployState = creator.deployState;
+ IndexedSearchCluster cluster = model.getContentClusters().get("test").getSearch().getIndexed();
+ assertEquals(3, cluster.getDocumentDbs().size());
+ NewDocumentType type1 = deployState.getDocumentModel().getDocumentManager().getDocumentType("type1");
+ NewDocumentType type2 = deployState.getDocumentModel().getDocumentManager().getDocumentType("type2");
+ NewDocumentType type3 = deployState.getDocumentModel().getDocumentManager().getDocumentType("type3");
+ assertNotNull(type1);
+ assertNotNull(type2);
+ assertNotNull(type3);
+ }
+
+ @Test
+ public void requireIndexedOnlyServices() throws ParseException, IOException, SAXException {
+ VespaModel model = getIndexedVespaModel();
+ HostResource h = model.getHostSystem().getHosts().get(0);
+ String [] expectedServices = {"logserver", "configserver", "adminserver", "slobrok",
+ "logd", "configproxy","config-sentinel", "filedistributorservice",
+ "qrserver", "fleetcontroller", "topleveldispatch", "docprocservice",
+ "storagenode", "searchnode", "distributor", "transactionlogserver"};
+ // TODO DomContentBuilderTest.assertServices(h, expectedServices);
+ Routing routing = model.getRouting();
+ assertNotNull(routing);
+ assertEquals("[]", routing.getErrors().toString());
+ assertEquals(1, routing.getProtocols().size());
+ DocumentProtocol protocol = (DocumentProtocol) routing.getProtocols().get(0);
+ RoutingTableSpec spec = protocol.getRoutingTableSpec();
+ assertEquals(2, spec.getNumHops());
+ assertEquals("docproc/cluster.test.indexing/chain.indexing", spec.getHop(0).getName());
+ assertEquals("indexing", spec.getHop(1).getName());
+
+ RouteSpec r;
+ r = spec.getRoute(0);
+ assertEquals("default", r.getName());
+ assertEquals(1, r.getNumHops());
+ assertEquals("indexing", r.getHop(0));
+ r = spec.getRoute(1);
+ assertEquals("storage/cluster.test", r.getName());
+ assertEquals(1, r.getNumHops());
+ assertEquals("route:test", r.getHop(0));
+ r = spec.getRoute(2);
+ assertEquals("test", r.getName());
+ assertEquals(1, r.getNumHops());
+ assertEquals("[MessageType:test]", r.getHop(0));
+ r = spec.getRoute(3);
+ assertEquals("test-direct", r.getName());
+ assertEquals(1, r.getNumHops());
+ assertEquals("[Content:cluster=test]", r.getHop(0));
+ r = spec.getRoute(4);
+ assertEquals("test-index", r.getName());
+ assertEquals(2, r.getNumHops());
+ assertEquals("docproc/cluster.test.indexing/chain.indexing", r.getHop(0));
+ assertEquals("[Content:cluster=test]", r.getHop(1));
+ }
+ @Test
+ public void requireProtonStreamingOnly() throws ParseException, IOException, SAXException
+ {
+ VespaModel model = getStreamingVespaModel();
+ HostResource h = model.getHostSystem().getHosts().get(0);
+ String [] expectedServices = {"logserver", "configserver", "adminserver", "slobrok",
+ "logd", "configproxy","config-sentinel", "filedistributorservice",
+ "qrserver", "storagenode", "searchnode", "distributor",
+ "transactionlogserver"};
+// TODO DomContentBuilderTest.assertServices(h, expectedServices);
+ ContentCluster s = model.getContentClusters().get("test");
+ assertFalse(s.getSearch().hasIndexedCluster());
+
+
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ s.getStorageNodes().getConfig(builder);
+ s.getStorageNodes().getChildren().get("3").getConfig(builder);
+ assertTrue(new StorServerConfig(builder).persistence_provider().rpc().connectspec().startsWith("tcp/localhost:191"));
+ }
+
+ @Test
+ public void requireCorrectClusterList() throws ParseException, IOException, SAXException
+ {
+ VespaModel model = getStreamingVespaModel();
+ ContentCluster s = model.getContentClusters().get("test");
+ assertNotNull(s);
+ assertFalse(s.getSearch().hasIndexedCluster());
+ ClusterListConfig config = model.getConfig(ClusterListConfig.class, VespaModel.ROOT_CONFIGID);
+ assertThat(config.storage().size(), is(1));
+ assertThat(config.storage(0).name(), is("test"));
+ assertThat(config.storage(0).configid(), is("test"));
+ }
+
+ @Test
+ public void testContentSummaryStore() throws ParseException, IOException, SAXException {
+ String services=
+ "<services version='1.0'>" +
+ "<admin version='2.0'><adminserver hostalias='node0' /></admin>" +
+ "<content id='docstore' version='1.0'>\n" +
+ " <redundancy>1</redundancy>\n" +
+ " <documents>\n" +
+ " <document mode='index' type='docstorebench'/>\n" +
+ " </documents>\n" +
+ " <group>\n" +
+ " <node distribution-key='0' hostalias='node0'/>\n" +
+ " </group>\n" +
+ " <engine>\n" +
+ " <proton>\n" +
+ " <searchable-copies>1</searchable-copies>\n" +
+ " <tuning>\n" +
+ " <searchnode>\n" +
+ " <summary>\n" +
+ " <store>\n" +
+ " <logstore>\n" +
+ " <chunk>\n" +
+ " <maxsize>2048</maxsize>\n" +
+ " </chunk>\n" +
+ " </logstore>\n" +
+ " </store>\n" +
+ " </summary>\n" +
+ " </searchnode>\n" +
+ " </tuning>\n" +
+ " </proton>\n" +
+ " </engine>\n" +
+ " </content>\n" +
+ " </services>";
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("docstorebench");
+ VespaModel model = new VespaModelCreatorWithMockPkg(getHosts(), services, sds).create();
+ ProtonConfig.Builder pb = new ProtonConfig.Builder();
+ model.getConfig(pb, "docstore/search/cluster.docstore/0");
+ }
+
+ @Test
+ public void testMixedIndexAndStoreOnly() throws ParseException, IOException, SAXException {
+ String services=
+ "<services version='1.0'>" +
+ " <admin version='2.0'><adminserver hostalias='node0' /></admin>" +
+ " <content id='docstore' version=\"1.0\">" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type=\"index_me\" mode=\"index\"/>" +
+ " <document type=\"store_me\" mode=\"store-only\"/>" +
+ " </documents>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"node0\"/>" +
+ " </group>" +
+ " </content>" +
+ "</services>";
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("index_me", "store_me");
+ VespaModel model = new VespaModelCreatorWithMockPkg(getHosts(), services, sds).create();
+ ProtonConfig.Builder pb = new ProtonConfig.Builder();
+ model.getConfig(pb, "docstore/search/cluster.docstore/0");
+ ProtonConfig protonConfig = new ProtonConfig(pb);
+ assertEquals(2, protonConfig.documentdb().size());
+ assertEquals("index_me", protonConfig.documentdb(0).inputdoctypename());
+ assertEquals("docstore/search/cluster.docstore/index_me", protonConfig.documentdb(0).configid());
+ assertEquals("store_me", protonConfig.documentdb(1).inputdoctypename());
+ assertEquals("docstore/search", protonConfig.documentdb(1).configid());
+ }
+
+ @Test
+ public void requireThatIndexingDocprocGetsConfigIdBasedOnDistributionKey() throws ParseException, IOException, SAXException {
+ VespaModel model = getIndexedVespaModel();
+ ContainerCluster cluster = model.getContainerClusters().get("cluster.test.indexing");
+ assertEquals("docproc/cluster.test.indexing/3", cluster.getContainers().get(0).getConfigId());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/IndexingAndDocprocRoutingTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexingAndDocprocRoutingTest.java
new file mode 100644
index 00000000000..bfb1eb2180a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/IndexingAndDocprocRoutingTest.java
@@ -0,0 +1,507 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.messagebus.routing.*;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.docproc.ContainerDocproc;
+import com.yahoo.vespa.model.container.docproc.DocprocChain;
+import com.yahoo.vespa.model.routing.DocumentProtocol;
+import com.yahoo.vespa.model.routing.Protocol;
+import com.yahoo.vespa.model.routing.Routing;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.*;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.13
+ */
+public class IndexingAndDocprocRoutingTest extends ContentBaseTest {
+ @Test
+ public void oneContentOneDoctypeImplicitIndexingClusterImplicitIndexingChain()
+ throws IOException, SAXException, ParseException {
+ final String CLUSTERNAME = "musiccluster";
+ SearchClusterSpec searchCluster = new SearchClusterSpec(CLUSTERNAME, null, null);
+ searchCluster.searchDefs.add(new SearchDefSpec("music", "artist", "album"));
+ VespaModel model = getIndexedContentVespaModel(Collections.<DocprocClusterSpec>emptyList(), Arrays.asList(searchCluster));
+ assertIndexing(model, new DocprocClusterSpec(CLUSTERNAME + ".indexing", new DocprocChainSpec("docproc/cluster." + CLUSTERNAME + ".indexing/chain.indexing")));
+ assertFeedingRoute(model, CLUSTERNAME, "docproc/cluster." + CLUSTERNAME + ".indexing/chain.indexing");
+ }
+
+ @Test
+ public void oneContentTwoDoctypesImplicitIndexingClusterImplicitIndexingChain()
+ throws IOException, SAXException, ParseException {
+ final String CLUSTERNAME = "musicandbookscluster";
+ SearchClusterSpec searchCluster = new SearchClusterSpec(CLUSTERNAME, null, null);
+ searchCluster.searchDefs.add(new SearchDefSpec("music", "artist", "album"));
+ searchCluster.searchDefs.add(new SearchDefSpec("book", "author", "title"));
+ VespaModel model = getIndexedContentVespaModel(Collections.<DocprocClusterSpec>emptyList(), Arrays.asList(searchCluster));
+ assertIndexing(model, new DocprocClusterSpec(CLUSTERNAME + ".indexing", new DocprocChainSpec("docproc/cluster." + CLUSTERNAME + ".indexing/chain.indexing")));
+ assertFeedingRoute(model, CLUSTERNAME, "docproc/cluster." + CLUSTERNAME + ".indexing/chain.indexing");
+ }
+
+ @Test
+ public void twoContentTwoDoctypesImplicitIndexingClusterImplicitIndexingChain()
+ throws IOException, SAXException, ParseException {
+ final String MUSIC = "musiccluster";
+ SearchClusterSpec musicCluster = new SearchClusterSpec(MUSIC, null, null);
+ musicCluster.searchDefs.add(new SearchDefSpec("music", "artist", "album"));
+
+ final String BOOKS = "bookscluster";
+ SearchClusterSpec booksCluster = new SearchClusterSpec(BOOKS, null, null);
+ booksCluster.searchDefs.add(new SearchDefSpec("book", "author", "title"));
+
+ VespaModel model = getIndexedContentVespaModel(Collections.<DocprocClusterSpec>emptyList(), Arrays.asList(musicCluster, booksCluster));
+
+ assertIndexing(model,
+ new DocprocClusterSpec(MUSIC + ".indexing", new DocprocChainSpec("docproc/cluster." + MUSIC + ".indexing/chain.indexing")),
+ new DocprocClusterSpec(BOOKS + ".indexing", new DocprocChainSpec("docproc/cluster." + BOOKS + ".indexing/chain.indexing")));
+
+ assertFeedingRoute(model, MUSIC, "docproc/cluster." + MUSIC + ".indexing/chain.indexing");
+ assertFeedingRoute(model, BOOKS, "docproc/cluster." + BOOKS + ".indexing/chain.indexing");
+ }
+
+
+ @Test
+ public void oneContentOneDoctypeExplicitIndexingClusterImplicitIndexingChain()
+ throws IOException, SAXException, ParseException {
+ final String CLUSTERNAME = "musiccluster";
+ SearchClusterSpec searchCluster = new SearchClusterSpec(CLUSTERNAME, "dpcluster", null);
+ searchCluster.searchDefs.add(new SearchDefSpec("music", "artist", "album"));
+ VespaModel model = getIndexedContentVespaModel(Arrays.asList(new DocprocClusterSpec("dpcluster")), Arrays.asList(searchCluster));
+ assertIndexing(model, new DocprocClusterSpec("dpcluster", new DocprocChainSpec("dpcluster/chain.indexing")));
+ assertFeedingRoute(model, CLUSTERNAME, "dpcluster/chain.indexing");
+ }
+
+ @Test
+ public void oneSearchOneDoctypeExplicitIndexingClusterExplicitIndexingChain()
+ throws IOException, SAXException, ParseException {
+ String xml =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<services version=\"1.0\">\n" +
+ " <admin version=\"2.0\">\n" +
+ " <adminserver hostalias=\"node0\"/> \n" +
+ " </admin>\n" +
+ "\n" +
+ " <content id=\"searchcluster\" version=\"1.0\">\n" +
+ " <redundancy>2</redundancy>\n" +
+ " <documents>\n" +
+ " <document-processing cluster='dpcluster' chain='fooindexing'/>\n" +
+ " <document type=\"music\" mode=\"index\"/>\n" +
+ " </documents>\n" +
+ " <nodes>\n" +
+ " <node hostalias=\"node0\" distribution-key=\"0\"/>\n" +
+ " </nodes>\n" +
+ " </content>\n" +
+ " \n" +
+ " <jdisc version='1.0' id='dpcluster'>\n" +
+ " <document-processing>\n" +
+ " <chain id='fooindexing' inherits='indexing '/>\n" +
+ " </document-processing>\n" +
+ " <nodes>\n" +
+ " <node hostalias='node0'/>\n" +
+ " </nodes>\n" +
+ " <http>\n" +
+ " <server id='dpcluster' port='8000'/>\n" +
+ " </http>\n" +
+ " </jdisc>\n" +
+ "</services>\n";
+ VespaModel model = getIndexedSearchVespaModel(xml);
+ assertIndexing(model, new DocprocClusterSpec("dpcluster", new DocprocChainSpec("dpcluster/chain.fooindexing", "indexing"),
+ new DocprocChainSpec("dpcluster/chain.indexing")));
+ assertFeedingRouteIndexed(model, "searchcluster", "dpcluster/chain.fooindexing");
+ }
+
+ @Test
+ public void twoContentTwoDoctypesExplicitIndexingInSameIndexingCluster()
+ throws IOException, SAXException, ParseException {
+ final String MUSIC = "musiccluster";
+ SearchClusterSpec musicCluster = new SearchClusterSpec(MUSIC, "dpcluster", null);
+ musicCluster.searchDefs.add(new SearchDefSpec("music", "artist", "album"));
+
+ final String BOOKS = "bookscluster";
+ SearchClusterSpec booksCluster = new SearchClusterSpec(BOOKS, "dpcluster", null);
+ booksCluster.searchDefs.add(new SearchDefSpec("book", "author", "title"));
+
+ VespaModel model = getIndexedContentVespaModel(Arrays.asList(new DocprocClusterSpec("dpcluster")),
+ Arrays.asList(musicCluster, booksCluster));
+
+ assertIndexing(model, new DocprocClusterSpec("dpcluster", new DocprocChainSpec("dpcluster/chain.indexing")));
+ assertFeedingRoute(model, MUSIC, "dpcluster/chain.indexing");
+ assertFeedingRoute(model, BOOKS, "dpcluster/chain.indexing");
+ }
+
+ @Test
+ public void noContentClustersOneDocprocCluster() throws ParseException, IOException, SAXException {
+ String services =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services version='1.0'>\n" +
+ " <admin version='2.0'>\n" +
+ " <adminserver hostalias='node0'/>\n" +
+ " </admin>\n" +
+ " <jdisc version='1.0' id='dokprok'>\n" +
+ " <document-processing />\n" +
+ " <nodes>\n" +
+ " <node hostalias='node0'/>\n" +
+ " </nodes>\n" +
+ " </jdisc>\n" +
+ "</services>\n";
+
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("music", "title", "artist");
+ VespaModel model = new VespaModelCreatorWithMockPkg(getHosts(),
+ services, sds).create();
+ assertIndexing(model, new DocprocClusterSpec("dokprok"));
+ }
+
+ @Test
+ public void twoContentTwoDoctypesExplicitIndexingInDifferentIndexingClustersExplicitChain()
+ throws IOException, SAXException, ParseException {
+ final String MUSIC = "musiccluster";
+ SearchClusterSpec musicCluster = new SearchClusterSpec(MUSIC, "dpmusiccluster", "dpmusicchain");
+ musicCluster.searchDefs.add(new SearchDefSpec("music", "artist", "album"));
+
+ final String BOOKS = "bookscluster";
+ SearchClusterSpec booksCluster = new SearchClusterSpec(BOOKS, "dpbookscluster", "dpbookschain");
+ booksCluster.searchDefs.add(new SearchDefSpec("book", "author", "title"));
+
+ DocprocClusterSpec dpMusicCluster = new DocprocClusterSpec("dpmusiccluster", new DocprocChainSpec("dpmusicchain", "indexing"));
+ DocprocClusterSpec dpBooksCluster = new DocprocClusterSpec("dpbookscluster", new DocprocChainSpec("dpbookschain", "indexing"));
+ VespaModel model = getIndexedContentVespaModel(Arrays.asList(
+ dpMusicCluster,
+ dpBooksCluster),
+ Arrays.asList(
+ musicCluster,
+ booksCluster));
+
+ //after we generated model, add indexing chains for validation:
+ dpMusicCluster.chains.clear();
+ dpMusicCluster.chains.add(new DocprocChainSpec("dpmusiccluster/chain.indexing"));
+ dpMusicCluster.chains.add(new DocprocChainSpec("dpmusiccluster/chain.dpmusicchain"));
+
+ dpBooksCluster.chains.clear();
+ dpBooksCluster.chains.add(new DocprocChainSpec("dpbookscluster/chain.indexing"));
+ dpBooksCluster.chains.add(new DocprocChainSpec("dpbookscluster/chain.dpbookschain"));
+
+ assertIndexing(model, dpMusicCluster, dpBooksCluster);
+ assertFeedingRoute(model, MUSIC, "dpmusiccluster/chain.dpmusicchain");
+ assertFeedingRoute(model, BOOKS, "dpbookscluster/chain.dpbookschain");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void twoContentTwoDoctypesExplicitIndexingInDifferentIndexingClustersExplicitChainIncorrectInheritance()
+ throws IOException, SAXException, ParseException {
+ final String MUSIC = "musiccluster";
+ SearchClusterSpec musicCluster = new SearchClusterSpec(MUSIC, "dpmusiccluster", "dpmusicchain");
+ musicCluster.searchDefs.add(new SearchDefSpec("music", "artist", "album"));
+
+ final String BOOKS = "bookscluster";
+ SearchClusterSpec booksCluster = new SearchClusterSpec(BOOKS, "dpbookscluster", "dpbookschain");
+ booksCluster.searchDefs.add(new SearchDefSpec("book", "author", "title"));
+
+ DocprocClusterSpec dpMusicCluster = new DocprocClusterSpec("dpmusiccluster", new DocprocChainSpec("dpmusicchain"));
+ DocprocClusterSpec dpBooksCluster = new DocprocClusterSpec("dpbookscluster", new DocprocChainSpec("dpbookschain"));
+ VespaModel model = getIndexedContentVespaModel(Arrays.asList(
+ dpMusicCluster,
+ dpBooksCluster),
+ Arrays.asList(
+ musicCluster,
+ booksCluster));
+
+ //after we generated model, add indexing chains for validation:
+ dpMusicCluster.chains.clear();
+ dpMusicCluster.chains.add(new DocprocChainSpec("dpmusiccluster/chain.indexing"));
+ dpMusicCluster.chains.add(new DocprocChainSpec("dpmusiccluster/chain.dpmusicchain"));
+
+ dpBooksCluster.chains.clear();
+ dpBooksCluster.chains.add(new DocprocChainSpec("dpbookscluster/chain.indexing"));
+ dpBooksCluster.chains.add(new DocprocChainSpec("dpbookscluster/chain.dpbookschain"));
+
+ assertIndexing(model, dpMusicCluster, dpBooksCluster);
+ assertFeedingRoute(model, MUSIC, "dpmusiccluster/chain.dpmusicchain");
+ assertFeedingRoute(model, BOOKS, "dpbookscluster/chain.dpbookschain");
+ }
+
+ private void assertIndexing(VespaModel model, DocprocClusterSpec... expectedDocprocClusters) {
+ Map<String, ContainerCluster> docprocClusters = getDocprocClusters(model);
+ assertThat(docprocClusters.size(), is(expectedDocprocClusters.length));
+
+ for (DocprocClusterSpec expectedDocprocCluster : expectedDocprocClusters) {
+ ContainerCluster docprocCluster = docprocClusters.get(expectedDocprocCluster.name);
+ assertThat(docprocCluster, not(nullValue()));
+ assertThat(docprocCluster.getName(), is(expectedDocprocCluster.name));
+ ContainerDocproc containerDocproc = docprocCluster.getDocproc();
+ assertThat(containerDocproc, not(nullValue()));
+ List<DocprocChain> chains = containerDocproc.getChains().allChains().allComponents();
+ assertThat(chains.size(), is(expectedDocprocCluster.chains.size()));
+ List<String> actualDocprocChains = new ArrayList<>();
+ for (DocprocChain chain : chains) {
+ actualDocprocChains.add(chain.getServiceName());
+ }
+ List<String> expectedDocprocChainStrings = new ArrayList<>();
+ for (DocprocChainSpec spec : expectedDocprocCluster.chains) {
+ expectedDocprocChainStrings.add(spec.name);
+ }
+
+ assertThat(actualDocprocChains, hasItems(expectedDocprocChainStrings.toArray(new String[0])));
+ }
+ }
+
+ private Map<String, ContainerCluster> getDocprocClusters(VespaModel model) {
+ Map<String, ContainerCluster> docprocClusters = new HashMap<>();
+ for (ContainerCluster containerCluster : model.getContainerClusters().values()) {
+ if (containerCluster.getDocproc() != null) {
+ docprocClusters.put(containerCluster.getName(), containerCluster);
+ }
+
+ }
+ return docprocClusters;
+ }
+
+ private void assertFeedingRoute(VespaModel model, String searchClusterName, String indexingHopName) {
+ Routing routing = model.getRouting();
+ List<Protocol> protocols = routing.getProtocols();
+
+ DocumentProtocol documentProtocol = null;
+ for (Protocol protocol : protocols) {
+ if (protocol instanceof DocumentProtocol) {
+ documentProtocol = (DocumentProtocol) protocol;
+ }
+ }
+
+ assertNotNull(documentProtocol);
+
+ RoutingTable table = new RoutingTable(documentProtocol.getRoutingTableSpec());
+
+ HopBlueprint indexingHop = table.getHop("indexing");
+
+ assertThat(indexingHop, not(nullValue()));
+
+ assertThat(indexingHop.getNumDirectives(), is(1));
+ assertThat(indexingHop.getDirective(0), instanceOf(PolicyDirective.class));
+ assertThat(indexingHop.getDirective(0).toString(), is("[DocumentRouteSelector]"));
+ //assertThat(indexingHop.getNumRecipients(), is(1));
+ //assertThat(indexingHop.getRecipient(0).getServiceName(), is(searchClusterName));
+
+ Route route = table.getRoute(searchClusterName);
+ assertNotNull(route);
+
+ assertThat(route.getNumHops(), is(1));
+ Hop messageTypeHop = route.getHop(0);
+ assertThat(messageTypeHop.getNumDirectives(), is(1));
+ assertThat(messageTypeHop.getDirective(0), instanceOf(PolicyDirective.class));
+ assertThat(messageTypeHop.getDirective(0).toString(), is("[MessageType:" + searchClusterName + "]"));
+ PolicyDirective messageTypeDirective = (PolicyDirective) messageTypeHop.getDirective(0);
+ assertThat(messageTypeDirective.getName(), is("MessageType"));
+ assertThat(messageTypeDirective.getParam(), is(searchClusterName));
+
+ String indexingRouteName = DocumentProtocol.getIndexedRouteName(model.getContentClusters().get(searchClusterName).getConfigId());
+ Route indexingRoute = table.getRoute(indexingRouteName);
+
+ assertThat(indexingRoute.getNumHops(), is(2));
+ assertThat(indexingRoute.getHop(0).getServiceName(), is(indexingHopName));
+ assertThat(indexingRoute.getHop(1), not(nullValue()));
+ }
+
+ private void assertFeedingRouteIndexed(VespaModel model, String searchClusterName, String indexingHopName) {
+ Routing routing = model.getRouting();
+ List<Protocol> protocols = routing.getProtocols();
+
+ DocumentProtocol documentProtocol = null;
+ for (Protocol protocol : protocols) {
+ if (protocol instanceof DocumentProtocol) {
+ documentProtocol = (DocumentProtocol) protocol;
+ }
+ }
+
+ assertNotNull(documentProtocol);
+
+ RoutingTable table = new RoutingTable(documentProtocol.getRoutingTableSpec());
+
+ Route indexingRoute = table.getRoute("searchcluster-index");
+ assertThat(indexingRoute.getNumHops(), is(2));
+ assertThat(indexingRoute.getHop(0).toString(), is(indexingHopName));
+ assertThat(indexingRoute.getHop(1).toString(), is("[Content:cluster=" + searchClusterName + "]"));
+ }
+
+
+ private String createVespaServices(String mainPre, String contentClusterPre, String contentClusterPost,
+ String searchClusterPre, String searchClusterPost, String searchClusterPostPost,
+ String mainPost, List<SearchClusterSpec> searchClusterSpecs) {
+ StringBuilder retval = new StringBuilder();
+ retval.append(mainPre);
+
+ for (SearchClusterSpec searchClusterSpec : searchClusterSpecs) {
+ retval.append(contentClusterPre).append(searchClusterSpec.name).append(contentClusterPost);
+ retval.append(searchClusterPre);
+ for (SearchDefSpec searchDefSpec : searchClusterSpec.searchDefs) {
+ retval.append(" <document type='")
+ .append(searchDefSpec.typeName)
+ .append("' mode='")
+ .append("index")
+ .append("' />\n");
+ }
+ if (searchClusterSpec.indexingClusterName != null) {
+ retval.append(" <document-processing cluster='").append(searchClusterSpec.indexingClusterName).append("'");
+ if (searchClusterSpec.indexingChainName != null) {
+ retval.append(" chain='").append(searchClusterSpec.indexingChainName).append("'");
+ }
+ retval.append("/>\n");
+ }
+ retval.append(searchClusterPost);
+ retval.append(searchClusterPostPost);
+ }
+
+ retval.append(mainPost);
+ System.err.println(retval);
+ return retval.toString();
+ }
+
+ private String createVespaServicesWithContent(List<DocprocClusterSpec> docprocClusterSpecs, List<SearchClusterSpec> searchClusterSpecs) {
+ String mainPre =
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services version='1.0'>\n" +
+
+ " <admin version='2.0'>\n" +
+ " <adminserver hostalias='node0'/>\n" +
+ " </admin>\n" +
+
+ " <jdisc version='1.0'>\n" +
+ " <search/>\n" +
+ " <nodes>\n" +
+ " <node hostalias='node0'/>\n" +
+ " </nodes>\n" +
+ " </jdisc>\n";
+ int clusterNo = 0;
+ for (DocprocClusterSpec docprocClusterSpec : docprocClusterSpecs) {
+ String docprocCluster = "";
+ docprocCluster += " <jdisc version='1.0' id='" + docprocClusterSpec.name + "'>\n";
+
+ if (docprocClusterSpec.chains != null && docprocClusterSpec.chains.size() > 0) {
+ docprocCluster += " <document-processing>\n";
+ for (DocprocChainSpec chain : docprocClusterSpec.chains) {
+ if (chain.inherits.isEmpty()) {
+ docprocCluster += " <chain id='" + chain.name + "'/>\n";
+ } else {
+ docprocCluster += " <chain id='" + chain.name + "'";
+ docprocCluster += " inherits='";
+
+ for (String inherit : chain.inherits) {
+ docprocCluster += inherit + " ";
+ }
+
+ docprocCluster += "'/>\n";
+ }
+ }
+ docprocCluster += " </document-processing>\n";
+ } else {
+ docprocCluster += " <document-processing/>\n";
+ }
+
+ docprocCluster += " <http>\n" +
+ " <server id='" + docprocClusterSpec.name + "' port='" + (8000 + 10 * clusterNo) + "'/>\n" +
+ " </http>\n";
+
+ docprocCluster += " <nodes>\n" +
+ " <node hostalias='node0'/>\n" +
+ " </nodes>\n" +
+ " </jdisc>\n";
+ mainPre += docprocCluster;
+ clusterNo++;
+ }
+
+ String contentClusterPre =
+ " <content version='1.0' id='";
+
+ String contentClusterPost = "'>\n";
+ String searchClusterPre =
+ " <redundancy>1</redundancy>\n" +
+ " <documents>\n";
+ String searchClusterPost =
+ " </documents>\n" +
+ " <group>\n" +
+ " <node hostalias='node0' distribution-key='0' />\n" +
+ " </group>\n";
+
+ String searchClusterPostPost = " </content>\n";
+
+ String mainPost =
+ "</services>\n";
+ return createVespaServices(mainPre, contentClusterPre, contentClusterPost, searchClusterPre,
+ searchClusterPost, searchClusterPostPost, mainPost, searchClusterSpecs);
+ }
+
+ private VespaModel getIndexedSearchVespaModel(String xml)
+ throws ParseException, IOException, SAXException {
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("music", "album", "artist");
+ return new VespaModelCreatorWithMockPkg(getHosts(), xml, sds).create();
+ }
+
+ private VespaModel getIndexedContentVespaModel(List<DocprocClusterSpec> docprocClusterSpecs, List<SearchClusterSpec> searchClusterSpecs)
+ throws ParseException, IOException, SAXException {
+ List<String> sds = new ArrayList<>();
+
+ for (SearchClusterSpec cluster : searchClusterSpecs) {
+ for (SearchDefSpec def : cluster.searchDefs) {
+ sds.add(ApplicationPackageUtils.generateSearchDefinition(def.typeName, def.field1Name, def.field2Name));
+ }
+ }
+
+ return new VespaModelCreatorWithMockPkg(getHosts(),
+ createVespaServicesWithContent(docprocClusterSpecs, searchClusterSpecs), sds).create();
+ }
+
+ private class SearchClusterSpec {
+ private final String name;
+ private List<SearchDefSpec> searchDefs = new ArrayList<>(2);
+ private String indexingClusterName;
+ private String indexingChainName;
+
+ private SearchClusterSpec(String name, String indexingClusterName, String indexingChainName) {
+ this.name = name;
+ this.indexingClusterName = indexingClusterName;
+ this.indexingChainName = indexingChainName;
+ }
+ }
+
+ private class SearchDefSpec {
+ private String typeName;
+ private String field1Name;
+ private String field2Name;
+
+ private SearchDefSpec(String typeName, String field1Name, String field2Name) {
+ this.typeName = typeName;
+ this.field1Name = field1Name;
+ this.field2Name = field2Name;
+ }
+ }
+
+ private class DocprocClusterSpec {
+ private final String name;
+ private final List<DocprocChainSpec> chains = new ArrayList<>();
+
+ private DocprocClusterSpec(String name, DocprocChainSpec ... chains) {
+ this.name = name;
+ this.chains.addAll(Arrays.asList(chains));
+ }
+ }
+
+ private class DocprocChainSpec {
+ private final String name;
+ private final List<String> inherits = new ArrayList<>();
+
+ private DocprocChainSpec(String name, String ... inherits) {
+ this.name = name;
+ this.inherits.addAll(Arrays.asList(inherits));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/SearchCoverageTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/SearchCoverageTest.java
new file mode 100644
index 00000000000..777a8269470
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/SearchCoverageTest.java
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class SearchCoverageTest {
+
+ @Test
+ public void requireThatAccessorWork() {
+ SearchCoverage coverage = new SearchCoverage.Builder()
+ .setMinimum(0.1)
+ .setMinWaitAfterCoverageFactor(0.2)
+ .setMaxWaitAfterCoverageFactor(0.3)
+ .build();
+ assertEquals(0.1, coverage.getMinimum(), 1E-6);
+ assertEquals(0.2, coverage.getMinWaitAfterCoverageFactor(), 1E-6);
+ assertEquals(0.3, coverage.getMaxWaitAfterCoverageFactor(), 1E-6);
+ }
+
+ @Test
+ public void requireThatDefaultsAreNull() {
+ SearchCoverage search = new SearchCoverage.Builder().build();
+ assertNull(search.getMinimum());
+ assertNull(search.getMinWaitAfterCoverageFactor());
+ assertNull(search.getMaxWaitAfterCoverageFactor());
+ }
+
+ @Test
+ public void requireThatInvalidMinimumCanNotBeSet() {
+ SearchCoverage.Builder coverage = new SearchCoverage.Builder();
+ coverage.setMinimum(0.5);
+ assertEquals(0.5, coverage.build().getMinimum(), 1E-6);
+ try {
+ coverage.setMinimum(-0.5);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Expected value in range [0, 1], got -0.5.", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMinimum(), 1E-6);
+ try {
+ coverage.setMinimum(1.5);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Expected value in range [0, 1], got 1.5.", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMinimum(), 1E-6);
+ }
+
+ @Test
+ public void requireThatInvalidMinWaitAfterCoverageFactorCanNotBeSet() {
+ SearchCoverage.Builder coverage = new SearchCoverage.Builder();
+ coverage.setMinWaitAfterCoverageFactor(0.5);
+ assertEquals(0.5, coverage.build().getMinWaitAfterCoverageFactor(), 1E-6);
+ try {
+ coverage.setMinWaitAfterCoverageFactor(-0.5);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Expected value in range [0, 1], got -0.5.", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMinWaitAfterCoverageFactor(), 1E-6);
+ try {
+ coverage.setMinWaitAfterCoverageFactor(1.5);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Expected value in range [0, 1], got 1.5.", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMinWaitAfterCoverageFactor(), 1E-6);
+ }
+
+ @Test
+ public void requireThatInvalidMaxWaitAfterCoverageFactorCanNotBeSet() {
+ SearchCoverage.Builder coverage = new SearchCoverage.Builder();
+ coverage.setMaxWaitAfterCoverageFactor(0.5);
+ assertEquals(0.5, coverage.build().getMaxWaitAfterCoverageFactor(), 1E-6);
+ try {
+ coverage.setMaxWaitAfterCoverageFactor(-0.5);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Expected value in range [0, 1], got -0.5.", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMaxWaitAfterCoverageFactor(), 1E-6);
+ try {
+ coverage.setMaxWaitAfterCoverageFactor(1.5);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Expected value in range [0, 1], got 1.5.", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMaxWaitAfterCoverageFactor(), 1E-6);
+ }
+
+ @Test
+ public void requireThatMinWaitCanNotBeSetLargerThanMaxWait() {
+ SearchCoverage.Builder coverage = new SearchCoverage.Builder();
+ coverage.setMaxWaitAfterCoverageFactor(0.5);
+ coverage.setMinWaitAfterCoverageFactor(0.4);
+ assertEquals(0.4, coverage.build().getMinWaitAfterCoverageFactor(), 1E-6);
+ coverage.setMinWaitAfterCoverageFactor(0.5);
+ assertEquals(0.5, coverage.build().getMinWaitAfterCoverageFactor(), 1E-6);
+ try {
+ coverage.setMinWaitAfterCoverageFactor(0.6);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Minimum wait (got 0.6) must be no larger than maximum wait (was 0.5).", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMinWaitAfterCoverageFactor(), 1E-6);
+ }
+
+ @Test
+ public void requireThatMaxWaitCanNotBeSetSmallerThanMaxWait() {
+ SearchCoverage.Builder coverage = new SearchCoverage.Builder();
+ coverage.setMinWaitAfterCoverageFactor(0.5);
+ coverage.setMaxWaitAfterCoverageFactor(0.6);
+ assertEquals(0.6, coverage.build().getMaxWaitAfterCoverageFactor(), 1E-6);
+ coverage.setMaxWaitAfterCoverageFactor(0.5);
+ assertEquals(0.5, coverage.build().getMaxWaitAfterCoverageFactor(), 1E-6);
+ try {
+ coverage.setMaxWaitAfterCoverageFactor(0.4);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Maximum wait (got 0.4) must be no smaller than minimum wait (was 0.5).", e.getMessage());
+ }
+ assertEquals(0.5, coverage.build().getMaxWaitAfterCoverageFactor(), 1E-6);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java
new file mode 100644
index 00000000000..95761de7331
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java
@@ -0,0 +1,360 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.core.StorIntegritycheckerConfig;
+import com.yahoo.vespa.config.content.core.StorVisitorConfig;
+import com.yahoo.vespa.config.content.StorFilestorConfig;
+import com.yahoo.vespa.config.content.core.StorServerConfig;
+import com.yahoo.vespa.config.content.PersistenceConfig;
+import com.yahoo.vespa.config.storage.StorDevicesConfig;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.documentmodel.NewDocumentType;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.storagecluster.StorageCluster;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import static org.junit.Assert.*;
+
+public class StorageClusterTest {
+
+ StorageCluster parse(String xml) {
+ MockRoot root = new MockRoot();
+ root.getDeployState().getDocumentModel().getDocumentManager().add(
+ new NewDocumentType(new NewDocumentType.Name("music"))
+ );
+ root.getDeployState().getDocumentModel().getDocumentManager().add(
+ new NewDocumentType(new NewDocumentType.Name("movies"))
+ );
+ Document doc = XML.getDocument(xml);
+ Element clusterElem = doc.getDocumentElement();
+ ContentCluster cluster = new ContentCluster.Builder(null, null).build(root, clusterElem);
+
+ root.freezeModelTopology();
+ return cluster.getStorageNodes();
+ }
+
+ @Test
+ public void testBasics() {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ parse("<content id=\"foofighters\"><documents/>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>\n").
+ getConfig(builder);
+
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals(false, config.is_distributor());
+ assertEquals("foofighters", config.cluster_name());
+ }
+
+ @Test
+ public void testMerges() {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ parse("" +
+ "<content id=\"foofighters\">\n" +
+ " <documents/>" +
+ " <tuning>" +
+ " <merges max-per-node=\"1K\" max-queue-size=\"10K\"/>\n" +
+ " </tuning>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</content>"
+ ).getConfig(builder);
+
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals(1024, config.max_merges_per_node());
+ assertEquals(1024*10, config.max_merge_queue_size());
+ }
+
+ @Test
+ public void testVisitors() {
+ StorVisitorConfig.Builder builder = new StorVisitorConfig.Builder();
+ parse(
+ "<cluster id=\"bees\">\n" +
+ " <documents/>" +
+ " <tuning>\n" +
+ " <visitors thread-count=\"7\" max-queue-size=\"1000\">\n" +
+ " <max-concurrent fixed=\"42\" variable=\"100\"/>\n" +
+ " </visitors>\n" +
+ " </tuning>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>"
+ ).getConfig(builder);
+
+ StorVisitorConfig config = new StorVisitorConfig(builder);
+ assertEquals(42, config.maxconcurrentvisitors_fixed());
+ assertEquals(100, config.maxconcurrentvisitors_variable());
+ assertEquals(7, config.visitorthreads());
+ assertEquals(1000, config.maxvisitorqueuesize());
+ }
+
+ @Test
+ public void testPersistenceThreads() {
+ StorFilestorConfig.Builder builder = new StorFilestorConfig.Builder();
+ parse(
+ "<cluster id=\"bees\">\n" +
+ " <documents/>" +
+ " <engine>" +
+ " <vds/>" +
+ " </engine>" +
+ " <tuning>\n" +
+ " <persistence-threads>\n" +
+ " <thread lowest-priority=\"VERY_LOW\" count=\"2\"/>\n" +
+ " <thread lowest-priority=\"VERY_HIGH\" count=\"1\"/>\n" +
+ " <thread count=\"1\"/>\n" +
+ " </persistence-threads>\n" +
+ " </tuning>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>"
+ ).getConfig(builder);
+
+ StorFilestorConfig config = new StorFilestorConfig(builder);
+
+ assertEquals(4, config.threads().size());
+ assertEquals(190, config.threads().get(0).lowestpri());
+ assertEquals(190, config.threads().get(1).lowestpri());
+ assertEquals(60, config.threads().get(2).lowestpri());
+ assertEquals(255, config.threads().get(3).lowestpri());
+
+ assertEquals(true, config.enable_multibit_split_optimalization());
+ }
+
+ @Test
+ public void testNoPersistenceThreads() {
+ StorFilestorConfig.Builder builder = new StorFilestorConfig.Builder();
+ parse(
+ "<cluster id=\"bees\">\n" +
+ " <documents/>" +
+ " <tuning>\n" +
+ " </tuning>\n" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>"
+ ).getConfig(builder);
+
+ StorFilestorConfig config = new StorFilestorConfig(builder);
+
+ assertEquals(0, config.threads().size());
+ }
+
+ @Test
+ public void testMaintenance() {
+ StorIntegritycheckerConfig.Builder builder = new StorIntegritycheckerConfig.Builder();
+ parse(
+ "<cluster id=\"bees\">\n" +
+ " <documents/>" +
+ " <tuning>" +
+ " <maintenance start=\"01:00\" stop=\"02:00\" high=\"tuesday\"/>\n" +
+ " </tuning>" +
+ " <group>" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>" +
+ " </group>" +
+ "</cluster>"
+ ).getConfig(builder);
+ StorIntegritycheckerConfig config = new StorIntegritycheckerConfig(builder);
+
+ assertEquals(60, config.dailycyclestart());
+ assertEquals(120, config.dailycyclestop());
+ assertEquals("rrRrrrr", config.weeklycycle());
+ }
+
+ @Test
+ public void testCapacity() {
+ Document doc = XML.getDocument(
+ "<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\" capacity=\"1.5\"/>\n" +
+ " <node distribution-key=\"2\" hostalias=\"mockhost\" capacity=\"2.0\"/>\n" +
+ " </group>\n" +
+ "</cluster>"
+ );
+
+ ContentCluster cluster = new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+
+ for (int i = 0; i < 3; ++i) {
+ StorageNode node = cluster.getStorageNodes().getChildren().get("" + i);
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ cluster.getStorageNodes().getConfig(builder);
+ node.getConfig(builder);
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals(1.0 + (double)i * 0.5, config.node_capacity(), 0.001);
+ }
+ }
+
+ @Test
+ public void testRootFolder() {
+ Document doc = XML.getDocument(
+ "<cluster id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</cluster>"
+ );
+
+ ContentCluster cluster = new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+
+ StorageNode node = cluster.getStorageNodes().getChildren().get("0");
+
+ {
+ StorDevicesConfig.Builder builder = new StorDevicesConfig.Builder();
+ node.getConfig(builder);
+ StorDevicesConfig config = new StorDevicesConfig(builder);
+ assertEquals(Defaults.getDefaults().vespaHome() + "var/db/vespa/vds/storage/storage/0", config.root_folder());
+ }
+
+ {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ cluster.getStorageNodes().getConfig(builder);
+ node.getConfig(builder);
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals(Defaults.getDefaults().vespaHome() + "var/db/vespa/vds/storage/storage/0", config.root_folder());
+ }
+
+ {
+ StorServerConfig.Builder builder = new StorServerConfig.Builder();
+ cluster.getDistributorNodes().getConfig(builder);
+ cluster.getDistributorNodes().getChildren().get("0").getConfig(builder);
+ StorServerConfig config = new StorServerConfig(builder);
+ assertEquals(Defaults.getDefaults().vespaHome() + "var/db/vespa/vds/storage/distributor/0", config.root_folder());
+ }
+ }
+
+ @Test
+ public void testGenericPersistenceTuning() {
+ Document doc = XML.getDocument(
+ "<cluster id=\"storage\">\n" +
+ "<documents/>" +
+ "<engine>\n" +
+ " <fail-partition-on-error>true</fail-partition-on-error>\n" +
+ " <revert-time>34m</revert-time>\n" +
+ " <recovery-time>5d</recovery-time>\n" +
+ "</engine>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ "</cluster>"
+ );
+
+ ContentCluster cluster = new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+
+ PersistenceConfig.Builder builder = new PersistenceConfig.Builder();
+ cluster.getStorageNodes().getConfig(builder);
+
+ PersistenceConfig config = new PersistenceConfig(builder);
+ assertEquals(true, config.fail_partition_on_error());
+ assertEquals(34 * 60, config.revert_time_period());
+ assertEquals(5 * 24 * 60 * 60, config.keep_remove_time_period());
+ }
+
+ @Test
+ public void requireThatUserDoesntSpecifyBothGroupAndNodes() {
+ Document doc = XML.getDocument(
+ "<cluster id=\"storage\">\n" +
+ "<engine>\n" +
+ " <fail-partition-on-error>true</fail-partition-on-error>\n" +
+ " <revert-time>34m</revert-time>\n" +
+ " <recovery-time>5d</recovery-time>\n" +
+ "</engine>" +
+ " <group>\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " <nodes>\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>\n" +
+ " </nodes>\n" +
+ "</cluster>"
+ );
+
+ try {
+ new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+ assertTrue(false);
+ } catch (Exception e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatGroupNamesMustBeUniqueAmongstSiblings() {
+ Document doc = XML.getDocument(
+ "<cluster id=\"storage\">\n" +
+ "<documents/>\n" +
+ " <group>\n" +
+ " <distribution partitions=\"*\"/>\n" +
+ " <group distribution-key=\"0\" name=\"bar\">\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " <group distribution-key=\"0\" name=\"bar\">\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ "</cluster>"
+ );
+ try {
+ new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+ fail("Did not get exception with duplicate group names");
+ } catch (RuntimeException e) {
+ assertEquals("Cluster 'storage' has multiple groups with name 'bar' in the same subgroup. " +
+ "Group sibling names must be unique.", e.getMessage());
+ }
+ }
+
+ @Test
+ public void requireThatGroupNamesCanBeDuplicatedAcrossLevels() {
+ Document doc = XML.getDocument(
+ "<cluster id=\"storage\">\n" +
+ "<documents/>\n" +
+ " <group>\n" +
+ " <distribution partitions=\"*\"/>\n" +
+ " <group distribution-key=\"0\" name=\"bar\">\n" +
+ " <group distribution-key=\"0\" name=\"foo\">\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ " <group distribution-key=\"0\" name=\"foo\">\n" +
+ " <group distribution-key=\"0\" name=\"bar\">\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ "</cluster>"
+ );
+ // Should not throw.
+ new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+ }
+
+ @Test
+ public void requireThatNestedGroupsRequireDistribution() {
+ Document doc = XML.getDocument(
+ "<cluster id=\"storage\">\n" +
+ "<documents/>\n" +
+ " <group>\n" +
+ " <group distribution-key=\"0\" name=\"bar\">\n" +
+ " <node distribution-key=\"0\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " <group distribution-key=\"0\" name=\"baz\">\n" +
+ " <node distribution-key=\"1\" hostalias=\"mockhost\"/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ "</cluster>"
+ );
+ try {
+ new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+ fail("Did not get exception with missing distribution element");
+ } catch (RuntimeException e) {
+ assertEquals("'distribution' attribute is required with multiple subgroups", e.getMessage());
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageContentTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageContentTest.java
new file mode 100644
index 00000000000..95b5d273291
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageContentTest.java
@@ -0,0 +1,177 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.documentapi.messagebus.protocol.DocumentrouteselectorpolicyConfig;
+import com.yahoo.messagebus.routing.RouteSpec;
+import com.yahoo.messagebus.routing.RoutingTableSpec;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.routing.DocumentProtocol;
+import com.yahoo.vespa.model.routing.Routing;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.*;
+
+import static org.junit.Assert.*;
+
+public class StorageContentTest extends ContentBaseTest {
+ // TODO: Test with document-definitions
+
+ private String createStorageVespaServices(String cluster1docs, String cluster2docs) {
+ return "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0'/>" +
+ " </admin>" +
+ " <content version='1.0' id='bar'>" +
+ " <redundancy>1</redundancy>\n" +
+ cluster1docs +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ " </content>" +
+ " <content version='1.0' id='zoo'>" +
+ " <redundancy>1</redundancy>\n" +
+ cluster2docs +
+ " <group>" +
+ " <node hostalias='node0' distribution-key='0' />" +
+ " </group>" +
+ "</content>" +
+ "</services>";
+ }
+
+ private VespaModel getStorageVespaModel(String cluster1docs, String cluster2docs) throws ParseException, IOException, SAXException {
+ List<String> sds = ApplicationPackageUtils.generateSearchDefinitions("type1", "type2", "type3");
+ return new VespaModelCreatorWithMockPkg(getHosts(), createStorageVespaServices(cluster1docs, cluster2docs), sds).create();
+ }
+
+ public void doTestRouting(String cluster1docs, String cluster2docs, String expectedRoutes) throws Exception {
+ VespaModel model = getStorageVespaModel(cluster1docs, cluster2docs);
+
+ if (expectedRoutes == null) {
+ return;
+ }
+
+ Routing routing = model.getRouting();
+ assertNotNull(routing);
+
+ assertEquals(0, routing.getErrors().size());
+ assertEquals(1, routing.getProtocols().size());
+ DocumentProtocol protocol = (DocumentProtocol) routing.getProtocols().get(0);
+
+ RoutingTableSpec spec = protocol.getRoutingTableSpec();
+ assertEquals(1, spec.getNumHops());
+ assertEquals("indexing", spec.getHop(0).getName());
+ assertEquals("[DocumentRouteSelector]", spec.getHop(0).getSelector());
+
+ Map<String, RouteSpec> routes = new TreeMap<>();
+
+ for (int i = 0; i < spec.getNumRoutes(); ++i) {
+ RouteSpec r = spec.getRoute(i);
+
+ routes.put(r.getName(), r);
+ }
+
+ {
+ RouteSpec r = routes.get("default");
+ assertEquals(1, r.getNumHops());
+ assertEquals("indexing", r.getHop(0));
+ }
+
+ Set<String> configuredRoutes = new TreeSet<>();
+
+ DocumentrouteselectorpolicyConfig.Builder builder = new DocumentrouteselectorpolicyConfig.Builder();
+ protocol.getConfig(builder);
+ DocumentrouteselectorpolicyConfig config = new DocumentrouteselectorpolicyConfig(builder);
+
+ for (DocumentrouteselectorpolicyConfig.Route r : config.route()) {
+ configuredRoutes.add(r.name() + " : " + r.selector());
+ }
+
+ StringBuilder routeStr = new StringBuilder();
+ for (String r : configuredRoutes) {
+ routeStr.append(r).append('\n');
+ }
+
+ assertEquals(expectedRoutes, routeStr.toString());
+ }
+
+ @Test
+ public void testDocumentTypesRouting() throws Exception {
+ String cluster1docs = "<documents>\n" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " <document type=\"type2\" mode=\"store-only\"/>\n" +
+ "</documents>\n";
+ String cluster2docs = "<documents>\n" +
+ " <document type=\"type3\" mode=\"store-only\"/>\n" +
+ "</documents>\n";
+ String expectedRoutes = "bar : (type1) OR (type2)\n" +
+ "zoo : (type3)\n";
+
+ doTestRouting(cluster1docs, cluster2docs, expectedRoutes);
+ }
+
+ @Test
+ public void testDocumentTypesAndLocalSelectionRouting() throws Exception {
+ String cluster1docs = "<documents>\n" +
+ " <document type=\"type1\" mode=\"store-only\" selection=\"1 != 2\"/>\n" +
+ " <document type=\"type2\" mode=\"store-only\" selection=\"now() &gt; 1000\"/>\n" +
+ "</documents>\n";
+ String cluster2docs = "<documents>\n" +
+ " <document type=\"type3\" mode=\"store-only\" selection=\"true\"/>\n" +
+ "</documents>\n";
+ String expectedRoutes = "bar : (type1 AND (1 != 2)) OR (type2 AND (now() > 1000))\n" +
+ "zoo : (type3 AND (true))\n";
+
+ doTestRouting(cluster1docs, cluster2docs, expectedRoutes);
+ }
+
+ @Test
+ public void testDocumentTypesAndGlobalSelection() throws Exception {
+ String cluster1docs = "<documents selection=\"5 != 6\">\n" +
+ " <document type=\"type1\" mode=\"store-only\" selection=\"type1.f1 == 'baz'\"/>\n" + // Can refer to own type
+ " <document type=\"type2\" mode=\"store-only\"/>\n" +
+ "</documents>\n";
+ String cluster2docs = "<documents selection=\"true\">\n" +
+ " <document type=\"type3\" mode=\"store-only\"/>\n" +
+ "</documents>\n";
+ String expectedRoutes = "bar : (5 != 6) AND ((type1 AND (type1.f1 == 'baz')) OR (type2))\n" +
+ "zoo : (true) AND ((type3))\n";
+
+ doTestRouting(cluster1docs, cluster2docs, expectedRoutes);
+ }
+
+ @Test
+ public void testIllegalDocumentTypesInSelection() throws Exception {
+ String localDefs = "<documents>\n" +
+ " <document type=\"type1\" mode=\"store-only\"/>\n" +
+ " <document type=\"type2\" mode=\"store-only\" selection=\"type1.bar == 'baz'\"/>\n" + // Not own type
+ "</documents>\n";
+ String globalDefs = "<documents selection=\"type3.foo\">\n" + // No doctypes allowed
+ " <document type=\"type3\" mode=\"store-only\"/>\n" +
+ "</documents>\n";
+ String expectedRoutes = null;
+
+ try {
+ // Local
+ doTestRouting(localDefs, localDefs, expectedRoutes);
+ fail("no exception thrown for doc type in local selection");
+ } catch (RuntimeException e) {
+ assertTrue(e.getMessage().contains("Selection for document type 'type2" +
+ "' can not contain references to other " +
+ "document types (found reference to type 'type1')"));
+ }
+
+ try {
+ // Global
+ doTestRouting(globalDefs, globalDefs, expectedRoutes);
+ fail("no exception thrown for doc type in global selection");
+ } catch (RuntimeException e) {
+ assertTrue(e.getMessage().contains("Document type references are not allowed"));
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageGroupTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageGroupTest.java
new file mode 100644
index 00000000000..da6636255ba
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageGroupTest.java
@@ -0,0 +1,160 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.content.StorDistributionConfig;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test for storage groups.
+ */
+public class StorageGroupTest {
+ ContentCluster parse(String xml) {
+ Document doc = XML.getDocument(xml);
+ return new ContentCluster.Builder(null, null).build(new MockRoot(), doc.getDocumentElement());
+ }
+
+ @Test
+ public void testSingleGroup() {
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ ContentCluster cluster = parse(
+ "<content id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <node jvmargs=\"foo\" hostalias=\"mockhost\" distribution-key=\"0\"/>\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"1\"/>\n" +
+ " </group>\n" +
+ "</content>"
+ );
+
+ cluster.getConfig(builder);
+
+ assertEquals("content", cluster.getStorageNodes().getChildren().get("0").getServicePropertyString("clustertype"));
+ assertEquals("storage", cluster.getStorageNodes().getChildren().get("0").getServicePropertyString("clustername"));
+ assertEquals("0", cluster.getStorageNodes().getChildren().get("0").getServicePropertyString("index"));
+
+ assertEquals("content", cluster.getDistributorNodes().getChildren().get("0").getServicePropertyString("clustertype"));
+ assertEquals("storage", cluster.getDistributorNodes().getChildren().get("0").getServicePropertyString("clustername"));
+ assertEquals("0", cluster.getDistributorNodes().getChildren().get("0").getServicePropertyString("index"));
+
+ StorDistributionConfig config = new StorDistributionConfig(builder);
+
+ assertEquals(1, config.group().size());
+ assertEquals("invalid", config.group(0).index());
+ assertEquals("invalid", config.group(0).name());
+ assertEquals(2, config.group(0).nodes().size());
+ assertEquals(0, config.group(0).nodes(0).index());
+ assertEquals(1, config.group(0).nodes(1).index());
+ //assertNotNull(cluster.getRootGroup().getNodes().get(0).getHost());
+ }
+
+ @Test
+ public void testNestedGroupsNoDistribution() {
+ try {
+ parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <group distribution-key=\"0\" name=\"base\">\n" +
+ " <group distribution-key=\"0\" name=\"sub1\">\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"1\"/>\n" +
+ " </group>\n" +
+ " <group distribution-key=\"1\" name=\"sub2\">\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"2\"/>\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"3\"/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ "</cluster>"
+ );
+ assertTrue(false);
+ } catch (Exception e) {
+ }
+ }
+
+ @Test
+ public void testNestedGroups() {
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <distribution partitions=\"1|*\"/>\n" +
+ " <group distribution-key=\"0\" name=\"sub1\">\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"0\"/>\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"1\"/>\n" +
+ " </group>\n" +
+ " <group distribution-key=\"1\" name=\"sub2\">\n" +
+ " <distribution partitions=\"1|*\"/>\n" +
+ " <group distribution-key=\"0\" name=\"sub3\">\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"2\"/>\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"3\"/>\n" +
+ " </group>\n" +
+ " <group distribution-key=\"1\" name=\"sub4\">\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"4\"/>\n" +
+ " <node hostalias=\"mockhost\" distribution-key=\"5\"/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ "</content>"
+ ).getConfig(builder);
+
+ StorDistributionConfig config = new StorDistributionConfig(builder);
+
+ assertEquals(5, config.group().size());
+ assertEquals("invalid", config.group(0).index());
+ assertEquals("0", config.group(1).index());
+ assertEquals("1", config.group(2).index());
+ assertEquals("1.0", config.group(3).index());
+ assertEquals("1.1", config.group(4).index());
+ assertEquals("invalid", config.group(0).name());
+ assertEquals("sub1", config.group(1).name());
+ assertEquals("sub2", config.group(2).name());
+ assertEquals("sub3", config.group(3).name());
+ assertEquals("sub4", config.group(4).name());
+ assertEquals(2, config.group(1).nodes().size());
+ assertEquals(0, config.group(1).nodes(0).index());
+ assertEquals(1, config.group(1).nodes(1).index());
+ assertEquals(0, config.group(2).nodes().size());
+ assertEquals(2, config.group(3).nodes().size());
+ assertEquals(2, config.group(3).nodes(0).index());
+ assertEquals(3, config.group(3).nodes(1).index());
+ assertEquals(2, config.group(4).nodes().size());
+ assertEquals(4, config.group(4).nodes(0).index());
+ assertEquals(5, config.group(4).nodes(1).index());
+
+ assertEquals("1|*", config.group(0).partitions());
+ }
+
+ @Test
+ public void testGroupCapacity() {
+ StorDistributionConfig.Builder builder = new StorDistributionConfig.Builder();
+ parse(
+ "<content version=\"1.0\" id=\"storage\">\n" +
+ " <documents/>" +
+ " <group>\n" +
+ " <distribution partitions=\"1|*\"/>\n" +
+ " <group distribution-key=\"0\" name=\"sub1\">\n" +
+ " <node hostalias=\"mockhost\" capacity=\"0.5\" distribution-key=\"0\"/>\n" +
+ " <node hostalias=\"mockhost\" capacity=\"1.5\" distribution-key=\"1\"/>\n" +
+ " </group>\n" +
+ " <group distribution-key=\"1\" name=\"sub2\">\n" +
+ " <node hostalias=\"mockhost\" capacity=\"2.0\" distribution-key=\"2\"/>\n" +
+ " <node hostalias=\"mockhost\" capacity=\"1.5\" distribution-key=\"3\"/>\n" +
+ " </group>\n" +
+ " </group>\n" +
+ "</content>"
+ ).getConfig(builder);
+
+ StorDistributionConfig config = new StorDistributionConfig(builder);
+
+ assertEquals(3, config.group().size());
+ assertEquals(5.5, config.group(0).capacity(), 0.001);
+ assertEquals(2, config.group(1).capacity(), 0.001);
+ assertEquals(3.5, config.group(2).capacity(), 0.001);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageNodeTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageNodeTest.java
new file mode 100644
index 00000000000..b0f2214d058
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageNodeTest.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.config.storage.StorDevicesConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author hakon
+ */
+public class StorageNodeTest {
+ private StorDevicesConfig getConfig(boolean useVdsEngine) {
+ String vdsConfig = useVdsEngine ? " <engine>" +
+ " <vds/>" +
+ " </engine>" : "";
+
+ String servicesXml = "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0'/>" +
+ " </admin>" +
+ " <content version='1.0' id='zoo'>" +
+ " <redundancy>1</redundancy>" +
+ " <nodes count='1' />" +
+ " <documents>" +
+ " <document type='type1' mode='streaming' />" +
+ " </documents>" +
+ vdsConfig +
+ " </content>" +
+ "</services>";
+ List<String> searchDefinitions = ApplicationPackageUtils.generateSearchDefinition("type1");
+ VespaModelCreatorWithMockPkg modelCreator =
+ new VespaModelCreatorWithMockPkg(null, servicesXml, searchDefinitions);
+ ApplicationPackage appPkg = modelCreator.appPkg;
+ boolean failOnOutOfCapacity = true;
+ InMemoryProvisioner provisioner =
+ new InMemoryProvisioner(failOnOutOfCapacity, "host1.yahoo.com", "host2.yahoo.com");
+ DeployProperties.Builder builder = new DeployProperties.Builder();
+ DeployProperties properties = builder.hostedVespa(true).build();
+ DeployState deployState = new DeployState.Builder()
+ .applicationPackage(appPkg)
+ .modelHostProvisioner(provisioner)
+ .properties(properties)
+ .build();
+ VespaModel model = modelCreator.create(true, deployState);
+ return model.getConfig(StorDevicesConfig.class, "zoo/storage/0");
+ }
+
+ @Test
+ public void verifyDiskPathConfigIsSetForVds() throws Exception {
+ StorDevicesConfig config = getConfig(true);
+ assertEquals(1, config.disk_path().size());
+ assertEquals(Defaults.getDefaults().vespaHome() + "var/db/vespa/vds/zoo/storage/0/disks/d0", config.disk_path(0));
+ }
+
+ @Test
+ public void verifyDiskPathConfigIsNotSetForNonHosted() throws Exception {
+ StorDevicesConfig config = getConfig(false);
+ assertEquals(0, config.disk_path().size());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/TuningDispatchTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/TuningDispatchTest.java
new file mode 100644
index 00000000000..2f692f40b0e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/TuningDispatchTest.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class TuningDispatchTest {
+
+ @Test
+ public void requireThatAccessorWork() {
+ TuningDispatch dispatch = new TuningDispatch.Builder()
+ .setMaxHitsPerPartition(69)
+ .setDispatchPolicy("round-robin")
+ .setMinGroupCoverage(7.5)
+ .setMinActiveDocsCoverage(12.5)
+ .build();
+ assertEquals(69, dispatch.getMaxHitsPerPartition().intValue());
+ assertEquals(7.5, dispatch.getMinGroupCoverage().doubleValue(), 0.0);
+ assertEquals(12.5, dispatch.getMinActiveDocsCoverage().doubleValue(), 0.0);
+ assertTrue(TuningDispatch.DispatchPolicy.ROUNDROBIN == dispatch.getDispatchPolicy());
+ }
+ @Test
+ public void requireThatRandomDispatchWork() {
+ TuningDispatch dispatch = new TuningDispatch.Builder()
+ .setDispatchPolicy("random")
+ .build();
+ assertTrue(TuningDispatch.DispatchPolicy.RANDOM == dispatch.getDispatchPolicy());
+ assertNull(dispatch.getMinGroupCoverage());
+ assertNull(dispatch.getMinActiveDocsCoverage());
+ }
+
+ @Test
+ public void requireThatDefaultsAreNull() {
+ TuningDispatch dispatch = new TuningDispatch.Builder().build();
+ assertNull(dispatch.getMaxHitsPerPartition());
+ assertTrue(TuningDispatch.DispatchPolicy.ROUNDROBIN == dispatch.getDispatchPolicy());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/VDSProviderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/VDSProviderTest.java
new file mode 100644
index 00000000000..bbbb50ce2c3
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/VDSProviderTest.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.vespa.config.storage.StorMemfilepersistenceConfig;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.engines.VDSEngine;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+import static org.junit.Assert.assertEquals;
+
+public class VDSProviderTest {
+ VDSEngine parse(String xml) {
+ Document doc = XML.getDocument(xml);
+ return new VDSEngine(null, new ModelElement(doc.getDocumentElement()));
+ }
+
+ @Test
+ public void testTuning() {
+ StorMemfilepersistenceConfig.Builder builder = new StorMemfilepersistenceConfig.Builder();
+
+ parse(
+ " <vds>\n" +
+ " <tuning>\n" +
+ " <disk-full-ratio>0.93</disk-full-ratio>\n" +
+ " <cache-size>1G</cache-size>\n" +
+ " </tuning>" +
+ "</vds>"
+ ).getConfig(builder);
+
+ StorMemfilepersistenceConfig config = new StorMemfilepersistenceConfig(builder);
+
+ assertEquals(0.93, config.disk_full_factor(), 0.01);
+ assertEquals(1024 * 1024 * 1024, config.cache_size());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/YamasConfigSnoopTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/YamasConfigSnoopTest.java
new file mode 100644
index 00000000000..103c6339fe9
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/YamasConfigSnoopTest.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content;
+
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.model.test.TestRoot;
+import com.yahoo.metrics.MetricsmanagerConfig;
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author havardpe
+ **/
+public class YamasConfigSnoopTest {
+
+ private TestRoot root;
+
+ public void initRoot(int interval) throws Exception {
+ TestDriver tester = new TestDriver();
+ root = tester.buildModel(getAdminXml(interval) + getContent());
+ }
+
+ private String getAdminXml(int interval) {
+ return ""
+ + "<admin version='2.0'>"
+ + " <adminserver hostalias='mockhost' />"
+ + " <yamas interval='" + interval + "' systemname='test' />"
+ + "</admin>";
+ }
+
+ private String getContent() {
+ return (
+ "<content version='1.0' id='search'>"+
+ " <documents/>"+
+ " <nodes>"+
+ " <node hostalias='mockhost' distribution-key='0' />"+
+ " </nodes>"+
+ "</content>");
+ }
+
+ private MetricsmanagerConfig getConfig() {
+ return root.getConfig(MetricsmanagerConfig.class, "search/storage/0");
+ }
+
+ @Test
+ public void correct_config_is_snooped() throws Exception {
+ initRoot(60);
+ assertThat(getConfig().snapshot().periods().size(), is(2));
+ assertThat(getConfig().snapshot().periods(0), is(60));
+ assertThat(getConfig().snapshot().periods(1), is(300));
+ }
+
+ @Test
+ public void correct_config_is_snooped_default_interval() throws Exception {
+ String getAdminXmlIntervalNotSpecified = "<admin version='2.0'>"
+ + " <adminserver hostalias='mockhost' />"
+ + "</admin>";
+
+ TestDriver tester = new TestDriver();
+ root = tester.buildModel(getAdminXmlIntervalNotSpecified + getContent());
+ assertThat(getConfig().snapshot().periods().size(), is(2));
+ assertThat(getConfig().snapshot().periods(0), is(60));
+ assertThat(getConfig().snapshot().periods(1), is(300));
+ }
+
+ @Test(expected = Exception.class)
+ public void invalid_model_1() throws Exception {
+ initRoot(120);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/ClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/ClusterTest.java
new file mode 100644
index 00000000000..e53c0038421
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/ClusterTest.java
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.model.content.Content;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import com.yahoo.vespa.model.search.SearchDefinition;
+import com.yahoo.vespa.model.search.Dispatch;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ClusterTest {
+
+ @Test
+ public void requireThatContentSearchIsApplied() throws ParseException {
+ ContentCluster cluster = newContentCluster(
+ "<search>" +
+ " <query-timeout>1.1</query-timeout>" +
+ " <visibility-delay>2.3</visibility-delay>" +
+ "</search>");
+ IndexedSearchCluster searchCluster = cluster.getSearch().getIndexed();
+ assertNotNull(searchCluster);
+ assertEquals(1.1, searchCluster.getQueryTimeout(), 1E-6);
+ assertEquals(2.3, searchCluster.getVisibilityDelay(), 1E-6);
+ ProtonConfig.Builder builder = new ProtonConfig.Builder();
+ cluster.getSearch().getConfig(builder);
+ ProtonConfig proton = new ProtonConfig(builder);
+ assertEquals(searchCluster.getVisibilityDelay(), proton.documentdb(0).visibilitydelay(), 1E-6);
+ }
+
+ @Test
+ public void requireThatSearchCoverageIsApplied() throws ParseException {
+ ContentCluster cluster = newContentCluster(
+ "<search>" +
+ " <coverage>" +
+ " <minimum>0.11</minimum>" +
+ " <min-wait-after-coverage-factor>0.23</min-wait-after-coverage-factor>" +
+ " <max-wait-after-coverage-factor>0.58</max-wait-after-coverage-factor>" +
+ " </coverage>" +
+ "</search>");
+ for (Dispatch tld : cluster.getSearch().getIndexed().getTLDs()) {
+ PartitionsConfig.Builder builder = new PartitionsConfig.Builder();
+ tld.getConfig(builder);
+ PartitionsConfig config = new PartitionsConfig(builder);
+ assertEquals(11.0, config.dataset(0).minimal_searchcoverage(), 1E-6);
+ assertEquals(0.23, config.dataset(0).higher_coverage_minsearchwait(), 1E-6);
+ assertEquals(0.58, config.dataset(0).higher_coverage_maxsearchwait(), 1E-6);
+ }
+ }
+
+ private static ContentCluster newContentCluster(String contentSearchXml) throws ParseException {
+ ApplicationPackage app = new MockApplicationPackage.Builder()
+ .withHosts(
+ "<hosts>" +
+ " <host name='localhost'><alias>my_host</alias></host>" +
+ "</hosts>")
+ .withServices(
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='my_host' />" +
+ " </admin>" +
+ " <content version='1.0'>" +
+ " <documents>" +
+ " <document mode='index' type='my_document' />" +
+ " </documents>" +
+ " <engine><proton /></engine>" +
+ " <group>" +
+ " <node hostalias='my_host' distribution-key='0' />" +
+ " </group>" +
+ contentSearchXml +
+ " </content>" +
+ "</services>")
+ .withSearchDefinitions(ApplicationPackageUtils.generateSearchDefinition("my_document"))
+ .build();
+ List<Content> contents = new TestDriver().buildModel(app).getConfigModels(Content.class);
+ assertEquals(1, contents.size());
+ return contents.get(0).getCluster();
+ }
+
+ private static SearchDefinition newSearchDefinition(String name) throws ParseException {
+ SearchBuilder builder = new SearchBuilder();
+ builder.importString("search " + name + " { document " + name + " { } }");
+ builder.build();
+ return new SearchDefinition(name, builder.getSearch(name));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilderTest.java
new file mode 100644
index 00000000000..e3b0120360e
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomContentSearchBuilderTest.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.content.ContentSearch;
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import org.apache.commons.io.input.CharSequenceInputStream;
+import org.junit.Test;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class DomContentSearchBuilderTest {
+
+ @Test
+ public void requireThatDefaultsAreNull() throws Exception {
+ ContentSearch search = newContentSearch(
+ "<content/>");
+ assertNull(search.getVisibilityDelay());
+ assertNull(search.getQueryTimeout());
+ }
+
+ @Test
+ public void requireThatEmptySearchIsSafe() throws Exception {
+ ContentSearch search = newContentSearch(
+ "<content>" +
+ " <search/>" +
+ "</content>");
+ assertNull(search.getVisibilityDelay());
+ assertNull(search.getQueryTimeout());
+ }
+
+ @Test
+ public void requireThatContentSearchCanBeBuilt() throws Exception {
+ ContentSearch search = newContentSearch(
+ "<content>" +
+ " <search>" +
+ " <query-timeout>1.1</query-timeout>" +
+ " <visibility-delay>2.3</visibility-delay>" +
+ " </search>" +
+ "</content>");
+ assertEquals(1.1, search.getQueryTimeout(), 1E-6);
+ assertEquals(2.3, search.getVisibilityDelay(), 1E-6);
+ }
+
+ private static ContentSearch newContentSearch(String xml) throws Exception {
+ return DomContentSearchBuilder.build(
+ new ModelElement(DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .parse(new CharSequenceInputStream(xml, StandardCharsets.UTF_8))
+ .getDocumentElement()));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilderTest.java
new file mode 100644
index 00000000000..435382c99b6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomSearchCoverageBuilderTest.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.SearchCoverage;
+import org.apache.commons.io.input.CharSequenceInputStream;
+import org.junit.Test;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class DomSearchCoverageBuilderTest {
+
+ @Test
+ public void requireThatDefaultsAreNull() throws Exception {
+ SearchCoverage coverage = newSearchCoverage(
+ "<content/>");
+ assertNull(coverage.getMinimum());
+ assertNull(coverage.getMinWaitAfterCoverageFactor());
+ assertNull(coverage.getMaxWaitAfterCoverageFactor());
+ }
+
+ @Test
+ public void requireThatEmptySearchIsSafe() throws Exception {
+ SearchCoverage coverage = newSearchCoverage(
+ "<content>" +
+ " <search/>" +
+ "</content>");
+ assertNull(coverage.getMinimum());
+ assertNull(coverage.getMinWaitAfterCoverageFactor());
+ assertNull(coverage.getMaxWaitAfterCoverageFactor());
+ }
+
+ @Test
+ public void requireThatEmptyCoverageIsSafe() throws Exception {
+ SearchCoverage coverage = newSearchCoverage(
+ "<content>" +
+ " <search>" +
+ " <coverage/>" +
+ " </search>" +
+ "</content>");
+ assertNull(coverage.getMinimum());
+ assertNull(coverage.getMinWaitAfterCoverageFactor());
+ assertNull(coverage.getMaxWaitAfterCoverageFactor());
+ }
+
+ @Test
+ public void requireThatSearchCoverageCanBeBuilt() throws Exception {
+ SearchCoverage coverage = newSearchCoverage(
+ "<content>" +
+ " <search>" +
+ " <coverage>" +
+ " <minimum>0.11</minimum>" +
+ " <min-wait-after-coverage-factor>0.23</min-wait-after-coverage-factor>" +
+ " <max-wait-after-coverage-factor>0.58</max-wait-after-coverage-factor>" +
+ " </coverage>" +
+ " </search>" +
+ "</content>");
+ assertEquals(0.11, coverage.getMinimum(), 1E-6);
+ assertEquals(0.23, coverage.getMinWaitAfterCoverageFactor(), 1E-6);
+ assertEquals(0.58, coverage.getMaxWaitAfterCoverageFactor(), 1E-6);
+ }
+
+ private static SearchCoverage newSearchCoverage(String xml) throws Exception {
+ return DomSearchCoverageBuilder.build(
+ new ModelElement(DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .parse(new CharSequenceInputStream(xml, StandardCharsets.UTF_8))
+ .getDocumentElement()));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilderTest.java
new file mode 100644
index 00000000000..7c31f05908d
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/cluster/DomTuningDispatchBuilderTest.java
@@ -0,0 +1,99 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.cluster;
+
+import com.yahoo.vespa.model.builder.xml.dom.ModelElement;
+import com.yahoo.vespa.model.content.TuningDispatch;
+import org.apache.commons.io.input.CharSequenceInputStream;
+import org.junit.Test;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class DomTuningDispatchBuilderTest {
+
+ @Test
+ public void requireThatDefaultsAreNull() throws Exception {
+ TuningDispatch dispatch = newTuningDispatch(
+ "<content/>");
+ assertNull(dispatch.getMaxHitsPerPartition());
+ }
+
+ @Test
+ public void requireThatEmptyTuningIsSafe() throws Exception {
+ TuningDispatch dispatch = newTuningDispatch(
+ "<content>" +
+ " <tuning/>" +
+ "</content>");
+ assertNull(dispatch.getMaxHitsPerPartition());
+ }
+
+ @Test
+ public void requireThatEmptydispatchIsSafe() throws Exception {
+ TuningDispatch dispatch = newTuningDispatch(
+ "<content>" +
+ " <tuning>" +
+ " <dispatch/>" +
+ " </tuning>" +
+ "</content>");
+ assertNull(dispatch.getMaxHitsPerPartition());
+ assertNull(dispatch.getMinGroupCoverage());
+ assertNull(dispatch.getMinActiveDocsCoverage());
+ assertTrue(TuningDispatch.DispatchPolicy.ROUNDROBIN == dispatch.getDispatchPolicy());
+ }
+
+ @Test
+ public void requireThatTuningDispatchCanBeBuilt() throws Exception {
+ TuningDispatch dispatch = newTuningDispatch(
+ "<content>" +
+ " <tuning>" +
+ " <dispatch>" +
+ " <max-hits-per-partition>69</max-hits-per-partition>" +
+ " <min-group-coverage>7.5</min-group-coverage>" +
+ " <min-active-docs-coverage>12.5</min-active-docs-coverage>" +
+ " </dispatch>" +
+ " </tuning>" +
+ "</content>");
+ assertEquals(69, dispatch.getMaxHitsPerPartition().intValue());
+ assertEquals(7.5, dispatch.getMinGroupCoverage().doubleValue(), 0.0);
+ assertEquals(12.5, dispatch.getMinActiveDocsCoverage().doubleValue(), 0.0);
+ }
+ @Test
+ public void requireThatTuningDispatchPolicyRoundRobin() throws Exception {
+ TuningDispatch dispatch = newTuningDispatch(
+ "<content>" +
+ " <tuning>" +
+ " <dispatch>" +
+ " <dispatch-policy>round-robin</dispatch-policy>" +
+ " </dispatch>" +
+ " </tuning>" +
+ "</content>");
+ assertTrue(TuningDispatch.DispatchPolicy.ROUNDROBIN == dispatch.getDispatchPolicy());
+ }
+ @Test
+ public void requireThatTuningDispatchPolicyRandom() throws Exception {
+ TuningDispatch dispatch = newTuningDispatch(
+ "<content>" +
+ " <tuning>" +
+ " <dispatch>" +
+ " <dispatch-policy>random</dispatch-policy>" +
+ " </dispatch>" +
+ " </tuning>" +
+ "</content>");
+ assertTrue(TuningDispatch.DispatchPolicy.RANDOM == dispatch.getDispatchPolicy());
+ }
+
+ private static TuningDispatch newTuningDispatch(String xml) throws Exception {
+ return DomTuningDispatchBuilder.build(
+ new ModelElement(DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .parse(new CharSequenceInputStream(xml, StandardCharsets.UTF_8))
+ .getDocumentElement()));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ApplicationPackageBuilder.java b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ApplicationPackageBuilder.java
new file mode 100644
index 00000000000..0e8d3cbbff7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ApplicationPackageBuilder.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.utils;
+
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for building an application package with content clusters (used for testing only).
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class ApplicationPackageBuilder {
+
+ private List<ContentClusterBuilder> contentClusters = new ArrayList<>();
+ private List<String> searchDefinitions = new ArrayList<>();
+
+ public ApplicationPackageBuilder() {
+ }
+
+ public ApplicationPackageBuilder addCluster(ContentClusterBuilder contentCluster) {
+ contentClusters.add(contentCluster);
+ return this;
+ }
+
+ public ApplicationPackageBuilder addSearchDefinition(String searchDefinition) {
+ searchDefinitions.add(searchDefinition);
+ return this;
+ }
+
+ public VespaModelCreatorWithMockPkg buildCreator() {
+ return new VespaModelCreatorWithMockPkg(null, getServices(), searchDefinitions);
+ }
+
+ private String getServices() {
+ return "<services version='1.0'>\n" +
+ contentClusters.stream().map(cluster -> cluster.getXml() + "\n").reduce("", (acc, element) -> acc + element) +
+ "</services>";
+ }
+
+}
+
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java
new file mode 100644
index 00000000000..580f4f648d6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterBuilder.java
@@ -0,0 +1,121 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.utils;
+
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import org.w3c.dom.Document;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Class for building a content cluster with indexed search (used for testing only).
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class ContentClusterBuilder {
+
+ private String name = "mycluster";
+ private int redundancy = 1;
+ private int searchableCopies = 1;
+ private List<String> docTypes = Arrays.asList("test");
+ private String groupXml = getSimpleGroupXml();
+ private Optional<String> dispatchXml = Optional.empty();
+ private Optional<Double> protonDiskLimit = Optional.empty();
+ private Optional<Double> protonMemoryLimit = Optional.empty();
+
+ public ContentClusterBuilder() {
+ }
+
+ public ContentClusterBuilder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public ContentClusterBuilder redundancy(int redundancy) {
+ this.redundancy = redundancy;
+ return this;
+ }
+
+ public ContentClusterBuilder searchableCopies(int searchableCopies) {
+ this.searchableCopies = searchableCopies;
+ return this;
+ }
+
+ public ContentClusterBuilder docTypes(List<String> docTypes) {
+ this.docTypes = docTypes;
+ return this;
+ }
+
+ public ContentClusterBuilder groupXml(String groupXml) {
+ this.groupXml = groupXml;
+ return this;
+ }
+
+ public ContentClusterBuilder dispatchXml(Optional<String> dispatchXml) {
+ this.dispatchXml = dispatchXml;
+ return this;
+ }
+
+ public ContentClusterBuilder protonDiskLimit(double diskLimit) {
+ protonDiskLimit = Optional.of(diskLimit);
+ return this;
+ }
+
+ public ContentClusterBuilder protonMemoryLimit(double memoryLimit) {
+ protonMemoryLimit = Optional.of(memoryLimit);
+ return this;
+ }
+
+ public ContentCluster build(MockRoot root) {
+ Document doc = XML.getDocument(getXml());
+ return new ContentCluster.Builder(null, null).build(root, doc.getDocumentElement());
+ }
+
+ public String getXml() {
+ String xml = "<content version='1.0' id='" + name + "'>\n" +
+ " <redundancy>" + redundancy + "</redundancy>\n" +
+ " <documents>\n" +
+ docTypes.stream().map(type -> " <document mode='index' type='" + type + "'/>\n").collect(Collectors.joining("\n")) +
+ " </documents>\n" +
+ " <engine>\n" +
+ " <proton>\n" +
+ " <searchable-copies>" + searchableCopies + "</searchable-copies>\n" +
+ getResourceLimitsXml(" ") +
+ " </proton>\n" +
+ " </engine>\n";
+ if (dispatchXml.isPresent()) {
+ xml += dispatchXml.get();
+ }
+ return xml + groupXml +
+ "</content>";
+ }
+
+ private static String getSimpleGroupXml() {
+ return " <group>\n" +
+ " <node distribution-key='0' hostalias='mockhost'/>\n" +
+ " </group>\n";
+ }
+
+ private String getResourceLimitsXml(String indent) {
+ if (protonDiskLimit.isPresent() || protonMemoryLimit.isPresent()) {
+ String xml = indent + "<resource-limits>\n" +
+ getXmlLine("disk", protonDiskLimit, indent + " ") +
+ getXmlLine("memory", protonMemoryLimit, indent + " ") +
+ indent + "</resource-limits>\n";
+ return xml;
+ }
+ return "";
+ }
+
+ private static String getXmlLine(String tag, Optional<Double> value, String indent) {
+ if (value.isPresent()) {
+ return indent + "<" + tag + ">" + value.get() + "</" + tag + ">\n";
+ }
+ return "";
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterUtils.java b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterUtils.java
new file mode 100644
index 00000000000..e64a39198b4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/ContentClusterUtils.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.utils;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.api.HostProvisioner;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.model.provision.SingleNodeProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.text.XML;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import org.w3c.dom.Document;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * For testing purposes only.
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class ContentClusterUtils {
+
+ public static MockRoot createMockRoot(String[] hosts) throws Exception {
+ return createMockRoot(hosts, ApplicationPackageUtils.generateSearchDefinition("test"));
+ }
+
+ private static MockRoot createMockRoot(HostProvisioner provisioner, List<String> searchDefinitions) {
+ ApplicationPackage applicationPackage = new MockApplicationPackage.Builder().withSearchDefinitions(searchDefinitions).build();
+ DeployState deployState = new DeployState.Builder()
+ .applicationPackage(applicationPackage)
+ .modelHostProvisioner(provisioner)
+ .build();
+ return new MockRoot("", deployState);
+
+ }
+ public static MockRoot createMockRoot(String[] hosts, List<String> searchDefinitions) throws Exception {
+ return createMockRoot(new InMemoryProvisioner(true, hosts), searchDefinitions);
+ }
+
+ public static MockRoot createMockRoot(List<String> searchDefinitions) {
+ return createMockRoot(new SingleNodeProvisioner(), searchDefinitions);
+ }
+
+ public static ContentCluster createCluster(String clusterXml, MockRoot root) throws Exception {
+ Document doc = XML.getDocument(clusterXml);
+ return new ContentCluster.Builder(null, null).build(root, doc.getDocumentElement());
+ }
+
+ public static ContentCluster createCluster(String clusterXml, List<String> searchDefinitions) throws Exception {
+ MockRoot root = createMockRoot(searchDefinitions);
+ ContentCluster cluster = createCluster(clusterXml, root);
+ root.freezeModelTopology();
+ cluster.validate();
+ return cluster;
+ }
+
+ public static ContentCluster createCluster(String clusterXml) throws Exception {
+ return createCluster(clusterXml, ApplicationPackageUtils.generateSearchDefinitions("test"));
+ }
+
+ public static String createClusterXml(String groupXml, int redundancy, int searchableCopies) {
+ return createClusterXml(groupXml, Optional.empty(), redundancy, searchableCopies);
+ }
+
+ public static String createClusterXml(String groupXml, Optional<String> dispatchXml, int redundancy, int searchableCopies) {
+ return new ContentClusterBuilder().
+ groupXml(groupXml).
+ dispatchXml(dispatchXml).
+ redundancy(redundancy).
+ searchableCopies(searchableCopies).getXml();
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/utils/SearchDefinitionBuilder.java b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/SearchDefinitionBuilder.java
new file mode 100644
index 00000000000..a0817bf7446
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/content/utils/SearchDefinitionBuilder.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.content.utils;
+
+/**
+ * Class for building a search definition (used for testing only).
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class SearchDefinitionBuilder {
+
+ private String name = "test";
+ private String content = "";
+
+ public SearchDefinitionBuilder() {
+ }
+
+ public SearchDefinitionBuilder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public SearchDefinitionBuilder content(String content) {
+ this.content = content;
+ return this;
+ }
+
+ public String build() {
+ return "search " + name + " {\n" +
+ " document " + name + " {\n" +
+ content + "\n" +
+ " }\n" +
+ "}";
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/filedistribution/FileDistributorTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/filedistribution/FileDistributorTestCase.java
new file mode 100644
index 00000000000..cb8b3e4bc02
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/filedistribution/FileDistributorTestCase.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.filedistribution;
+
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.application.provider.MockFileRegistry;
+import com.yahoo.config.model.test.MockHosts;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bratseth
+ */
+public class FileDistributorTestCase {
+ @Test
+ public void fileDistributor() {
+ MockHosts hosts = new MockHosts();
+
+ FileDistributor fileDistributor = new FileDistributor(new MockFileRegistry());
+
+ FileReference ref1 = fileDistributor.sendFileToHosts("components/path1", Arrays.asList(hosts.host1, hosts.host2));
+ FileReference ref2 = fileDistributor.sendFileToHosts("path2", Arrays.asList(hosts.host3));
+
+ assertEquals(new HashSet<>(Arrays.asList(hosts.host1, hosts.host2, hosts.host3)),
+ fileDistributor.getTargetHosts());
+
+ assertTrue( ref1 != null );
+ assertTrue( ref2 != null );
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesModelTest.java b/config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesModelTest.java
new file mode 100644
index 00000000000..c7c1ae6784f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesModelTest.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic;
+
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.MapConfigModelRegistry;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class GenericServicesModelTest {
+
+ @Test
+ public void test_generic_services_builder() {
+ GenericServicesBuilder builder = new GenericServicesBuilder();
+ assertThat(builder.handlesElements().size(), is(1));
+ assertThat(builder.handlesElements().get(0), is(ConfigModelId.fromName("service")));
+ }
+
+ @Test
+ public void test_generic_services_model() {
+ MockRoot root = new MockRoot();
+ GenericServicesModel model = new GenericServicesModel(ConfigModelContext.createFromParentAndId(null, root, "foo"));
+ assertThat(model.serviceClusters().size(), is(0));
+ model.addCluster(new ServiceCluster(root, "mycluster", "/bin/foo"));
+ assertThat(model.serviceClusters().size(), is(1));
+ assertThat(model.serviceClusters().get(0).getName(), is("mycluster"));
+ }
+
+ @Test
+ public void test_generic_services_parsing() throws IOException, SAXException {
+ final String hosts =
+ "<hosts>" +
+ "<host name=\"localhost\">" +
+ " <alias>mockhost</alias>" +
+ " </host> " +
+ "</hosts>";
+ String services = "<services version=\"1.0\">"
+ + "<service id=\"me\" name=\"foo\" command=\"/bin/bar\" version=\"1.0\">"
+ + "<node hostalias=\"mockhost\" />"
+ + "</service>"
+ + "</services>";
+ VespaModel model = new VespaModel(new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).build());
+ GenericServicesModel gsModel = (GenericServicesModel) model.configModelRepo().get("me");
+ assertThat(gsModel.serviceClusters().size(), is(1));
+ assertThat(gsModel.serviceClusters().get(0).getName(), is("foo"));
+ assertThat(gsModel.serviceClusters().get(0).services().size(), is(1));
+ }
+
+} \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesTest.java b/config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesTest.java
new file mode 100644
index 00000000000..05c007b155b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/generic/GenericServicesTest.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.generic;
+
+import com.yahoo.cloud.config.SentinelConfig;
+import com.yahoo.config.codegen.CNode;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.ConfigPayloadBuilder;
+import com.yahoo.vespa.config.GenericConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.generic.service.Service;
+import com.yahoo.vespa.model.generic.service.ServiceCluster;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests that generic services result in correct sentinel config settings
+ *
+ * @author vegardh
+ */
+public class GenericServicesTest {
+
+ private static VespaModel model;
+
+ @BeforeClass
+ public static void getModel() throws IOException, SAXException {
+ String appDir = "src/test/cfg/application/app_genericservices";
+ ApplicationPackage app = FilesApplicationPackage.fromFile(new File(appDir));
+ model = new VespaModel(app);
+ }
+
+ @Test
+ public void testServicesSentinelConfig() throws IOException, SAXException {
+ String sentinelConfigId1="hosts/bogusname1/sentinel";
+ String sentinelConfigId2="hosts/bogusname2/sentinel";
+ String sentinelConfigId3="hosts/bogusname3/sentinel";
+ String sentinelConfigId4="hosts/bogusname4/sentinel";
+ SentinelConfig sentinel1 = model.getConfig(SentinelConfig.class, sentinelConfigId1);
+ SentinelConfig sentinel2 = model.getConfig(SentinelConfig.class, sentinelConfigId2);
+ SentinelConfig sentinel3 = model.getConfig(SentinelConfig.class, sentinelConfigId3);
+ SentinelConfig sentinel4 = model.getConfig(SentinelConfig.class, sentinelConfigId4);
+
+ assertServiceExists(sentinel1, "myservice", "mycmd1.sh", "myservice/0", true, true);
+ assertServiceExists(sentinel2, "myservice", "mycmd1.sh", "myservice/1", true, true);
+ assertServiceExists(sentinel3, "myservice", "mycmd1.sh", "myservice/2", true, true);
+ assertServiceExists(sentinel3, "myservice2", "mycmd1.sh", "myservice/3", true, true);
+ assertServiceExists(sentinel3, "myotherservice", "/home/vespa/bin/mycmd2.sh --ytest $FOO_BAR", "myotherservice/0", true, true);
+ assertServiceExists(sentinel4, "myotherservice", "/home/vespa/bin/mycmd2.sh --ytest $FOO_BAR", "myotherservice/1", true, true);
+ }
+
+ private void assertServiceExists(SentinelConfig sentinel, String serviceName, String cmd, String configId, boolean autostart, boolean autorestart) {
+ boolean matches = false;
+ Iterator<SentinelConfig.Service> it = sentinel.service().iterator();
+ while (!matches && it.hasNext()) {
+ SentinelConfig.Service service = it.next();
+ matches = service.autorestart() == autorestart &&
+ service.autostart() == autostart &&
+ service.name().equals(serviceName) &&
+ service.id().equals(configId) &&
+ service.command().equals(cmd);
+ }
+ assertTrue(matches);
+ }
+
+ @Test
+ public void testServicesModel() throws IOException, SAXException {
+ // Testing that this model can be constructed only for now
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/routing/test/RoutingTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/routing/test/RoutingTestCase.java
new file mode 100755
index 00000000000..fba740567f8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/routing/test/RoutingTestCase.java
@@ -0,0 +1,203 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.routing.test;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.documentapi.messagebus.protocol.DocumentrouteselectorpolicyConfig;
+import com.yahoo.io.IOUtils;
+import com.yahoo.messagebus.MessagebusConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static helpers.CompareConfigTestHelper.assertSerializedConfigEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RoutingTestCase {
+
+ private static final boolean WRITE_FILES = false;
+
+ @Test
+ @Ignore // TODO: Why?
+ public void testRoutingContent() throws IOException {
+ assertApplication(new File("src/test/cfg/routing/contentsimpleconfig"));
+ assertApplication(new File("src/test/cfg/routing/content_two_clusters"));
+ }
+
+ @Test
+ public void testRouting() throws IOException {
+ assertApplication(new File("src/test/cfg/routing/unexpectedrecipient"));
+ assertApplication(new File("src/test/cfg/routing/servicenotfound"));
+ assertApplication(new File("src/test/cfg/routing/routenotfoundinroute"));
+ assertApplication(new File("src/test/cfg/routing/routenotfound"));
+ assertApplication(new File("src/test/cfg/routing/routeconfig"));
+ assertApplication(new File("src/test/cfg/routing/replaceroute"));
+ assertApplication(new File("src/test/cfg/routing/replacehop"));
+ assertApplication(new File("src/test/cfg/routing/mismatchedrecipient"));
+ assertApplication(new File("src/test/cfg/routing/hopnotfound"));
+ assertApplication(new File("src/test/cfg/routing/hoperrorinroute"));
+ assertApplication(new File("src/test/cfg/routing/hoperrorinrecipient"));
+ assertApplication(new File("src/test/cfg/routing/hoperror"));
+ assertApplication(new File("src/test/cfg/routing/hopconfig"));
+ assertApplication(new File("src/test/cfg/routing/emptyroute"));
+ assertApplication(new File("src/test/cfg/routing/emptyhop"));
+ assertApplication(new File("src/test/cfg/routing/duplicateroute"));
+ assertApplication(new File("src/test/cfg/routing/duplicatehop"));
+ assertApplication(new File("src/test/cfg/routing/defaultconfig"));
+ }
+
+ /**
+ * Tests whether or not the given application produces the expected output. When creating new tests, create an
+ * application directory containing the necessary setup files, and call this method with a TRUE create flag.
+ *
+ * @param application The application directory.
+ * @throws IOException
+ */
+ private static void assertApplication(File application) throws IOException {
+ assertTrue(application.isDirectory());
+ String applicationName = application.getName();
+ System.out.println("Testing route configuration from application '" + applicationName + "'..");
+ Map<String, File> files = new HashMap<>();
+ for (File file : application.listFiles(new ContentFilter())) {
+ files.put(file.getName(), file);
+ }
+ String path = null;
+ try {
+ path = application.getCanonicalPath();
+ } catch (IOException e) {
+ fail("Could not resolve path for application '" + applicationName + "'.");
+ }
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg(path);
+
+ VespaModel model = creator.create();
+ List<String> errors = model.getRouting().getErrors();
+ if (errors.isEmpty()) {
+ if (files.containsKey("errors.txt")) {
+ if (WRITE_FILES) {
+ files.remove("errors.txt").delete();
+ } else {
+ fail("Route verification did not fail.");
+ }
+ }
+ MessagebusConfig.Builder mBusB = new MessagebusConfig.Builder();
+ model.getConfig(mBusB, "");
+ MessagebusConfig mBus = new MessagebusConfig(mBusB);
+ assertConfigFileContains(application, files, "messagebus.cfg", mBus);
+
+ DocumentrouteselectorpolicyConfig.Builder drB = new DocumentrouteselectorpolicyConfig.Builder();
+ model.getConfig(drB, "");
+ DocumentrouteselectorpolicyConfig dr = new DocumentrouteselectorpolicyConfig(drB);
+ assertConfigFileContains(application, files, "documentrouteselectorpolicy.cfg", dr);
+ } else {
+ StringBuilder msg = new StringBuilder();
+ for (String error : errors) {
+ msg.append(error).append("\n");
+ }
+ assertFileContains(application, files, "errors.txt", msg.toString());
+ }
+ System.out.println("\tDone.");
+ }
+
+ /**
+ * Tests whether or not a given file exists and contains some expected content.
+ *
+ * @param application The application directory.
+ * @param files The filtered list of files within the application.
+ * @param fileName The name of the file whose content to check.
+ * @param expectedContent The content required in the file being checked.
+ */
+ private static void assertFileContains(File application, Map<String, File> files,
+ String fileName, String expectedContent) {
+ if (WRITE_FILES) {
+ files.put(fileName, writeFile(application, fileName, expectedContent.trim() + "\n"));
+ }
+ if (!files.containsKey(fileName)) {
+ fail("Expected file '" + fileName + "' not found.\nExpected content: " + expectedContent);
+ return;
+ }
+ System.out.println("\tVerifying content of '" + fileName + "'.");
+ StringBuilder content = new StringBuilder();
+
+ try {
+ BufferedReader reader = new BufferedReader(new FileReader(files.get(fileName)));
+ String line = reader.readLine();
+ while (line != null) {
+ content.append(line).append("\n");
+ line = reader.readLine();
+ }
+ reader.close();
+ } catch (FileNotFoundException e) {
+ fail("File '" + fileName + "' not found.");
+ } catch (IOException e) {
+ fail("Failed to read content of file '" + fileName + ".");
+ }
+
+ assertEquals(content.toString().trim(), expectedContent.replace("\r", "").trim());
+ }
+ /**
+ * Tests whether or not a given file exists and contains some expected content.
+ *
+ * @param application The application directory.
+ * @param files The filtered list of files within the application.
+ * @param fileName The name of the file whose content to check.
+ * @param config The config required in the file being checked.
+ * @throws IOException
+ */
+ private static void assertConfigFileContains(File application, Map<String, File> files,
+ String fileName, ConfigInstance config) throws IOException {
+ final String configString = config.toString();
+ if (WRITE_FILES) {
+ files.put(fileName, writeFile(application, fileName, configString.trim() + "\n"));
+ }
+ if (!files.containsKey(fileName)) {
+ fail("Expected file '" + fileName + "' not found.");
+ return;
+ }
+ System.out.println("\tVerifying content of '" + fileName + "'.");
+ assertSerializedConfigEquals(IOUtils.readFile(files.get(fileName)), configString);
+ }
+
+ /**
+ * Writes content to a specific file.
+ *
+ * @param application The application directory.
+ * @param name The name of the file to write.
+ * @param content The content to write.
+ * @return The file written.
+ */
+ private static File writeFile(File application, String name, String content) {
+ File ret = null;
+ try {
+ name = application.getCanonicalPath() + "/" + name;
+ System.out.println("\tWriting file '" + name + "'.");
+
+ PrintWriter writer = new PrintWriter(new FileWriter(name));
+ writer.print(content);
+ writer.close();
+
+ ret = new File(name);
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ return ret;
+ }
+
+ /**
+ * Helper class to filter what files within an application directory gets added to the files index.
+ */
+ private static class ContentFilter implements FilenameFilter {
+ public boolean accept(File file, String name) {
+ return !name.equals(".git") && !name.equals(".svn");
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/MultilevelDispatchTest.java b/config-model/src/test/java/com/yahoo/vespa/model/search/MultilevelDispatchTest.java
new file mode 100644
index 00000000000..135d5302aee
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/search/MultilevelDispatchTest.java
@@ -0,0 +1,382 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.model.test.TestRoot;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.SimpleConfigProducer;
+import com.yahoo.vespa.model.content.DispatchSpec;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.content.utils.ContentClusterUtils;
+import com.yahoo.vespa.model.search.utils.DispatchUtils;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static com.yahoo.vespa.model.content.utils.ContentClusterUtils.createClusterXml;
+import static com.yahoo.vespa.model.search.utils.DispatchUtils.getDataset;
+import static com.yahoo.vespa.model.search.utils.DispatchUtils.getFdispatchrcConfig;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+/**
+ * Unit tests for multi-level dispatchers in an indexed content cluster.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class MultilevelDispatchTest {
+
+ private static class EngineAsserter {
+ private List<PartitionsConfig.Dataset.Engine> engines;
+ private int engineIdx = 0;
+ public EngineAsserter(int numParts, int numEngines, Dispatch dispatch) {
+ PartitionsConfig.Dataset dataset = getDataset(dispatch);
+ assertEquals(numParts, dataset.numparts());
+ assertEquals(PartitionsConfig.Dataset.Querydistribution.AUTOMATIC, dataset.querydistribution());
+ engines = dataset.engine();
+ assertEquals(numEngines, engines.size());
+ }
+ EngineAsserter assertEngine(int rowId, int partitionId, String connectSpec) {
+ DispatchUtils.assertEngine(rowId, partitionId, connectSpec, engines.get(engineIdx++));
+ return this;
+ }
+ }
+
+ private String getGroupXml() {
+ return " <group>\n" +
+ " <node distribution-key='10' hostalias='mh0'/>\n" +
+ " <node distribution-key='11' hostalias='mh1'/>\n" +
+ " <node distribution-key='12' hostalias='mh2'/>\n" +
+ " <node distribution-key='13' hostalias='mh3'/>\n" +
+ " <node distribution-key='14' hostalias='mh4'/>\n" +
+ " <node distribution-key='15' hostalias='mh5'/>\n" +
+ " </group>\n";
+ }
+
+ private String getSimpleDispatchXml() {
+ return " <dispatch>\n" +
+ " <num-dispatch-groups>2</num-dispatch-groups>\n" +
+ " </dispatch>\n";
+ }
+
+ private String getDispatchXml() {
+ return " <dispatch>\n" +
+ " <group>\n" +
+ " <node distribution-key='10'/>\n" +
+ " <node distribution-key='12'/>\n" +
+ " <node distribution-key='14'/>\n" +
+ " </group>\n" +
+ " <group>\n" +
+ " <node distribution-key='11'/>\n" +
+ " <node distribution-key='13'/>\n" +
+ " <node distribution-key='15'/>\n" +
+ " </group>\n" +
+ " </dispatch>\n";
+ }
+
+ private ContentCluster createCluster(String dispatchXml) throws Exception {
+ String[] hosts = {"mh0", "mh1", "mh2", "mh3", "mh4", "mh5"};
+ MockRoot root = ContentClusterUtils.createMockRoot(hosts);
+ ContentCluster cluster = ContentClusterUtils.createCluster(createClusterXml(getGroupXml(), Optional.of(dispatchXml), 1, 1), root);
+
+ AbstractConfigProducer<Dispatch> dispatchParent = new SimpleConfigProducer<>(root, "tlds");
+ HostResource hostResource = new HostResource(new Host(root, "mockhost"));
+ IndexedSearchCluster index = cluster.getSearch().getIndexed();
+ index.addTld(dispatchParent, hostResource);
+ index.setupDispatchGroups();
+
+ root.freezeModelTopology();
+ cluster.validate();
+ return cluster;
+ }
+
+ private List<Dispatch> getDispatchers(Dispatch tld) {
+ DispatchGroup group = tld.getDispatchGroup();
+ List<Dispatch> dispatchers = new ArrayList<>();
+ for (SearchInterface dispatch : group.getSearchersIterable()) {
+ dispatchers.add((Dispatch)dispatch);
+ }
+ return dispatchers;
+ }
+
+ private void assertDispatchAndSearchNodes(int partId, Dispatch[] dispatchers, String[] connectSpecs, SearchNode[] searchNodes) {
+ assertEquals(dispatchers.length, connectSpecs.length);
+ assertEquals(connectSpecs.length, searchNodes.length);
+ int searchNodeIdx = 0;
+ for (int rowId = 0; rowId < dispatchers.length; ++rowId) {
+ assertDispatchAndSearchNodes(rowId, partId, searchNodes[searchNodeIdx++].getDistributionKey(),
+ dispatchers[rowId], connectSpecs, searchNodes);
+ }
+ }
+
+ private void assertDispatchAndSearchNodes(int expRowId, int expPartId, int expDistributionKey, Dispatch dispatch, String[] connectSpecs, SearchNode[] searchNodes) {
+ assertEquals(expRowId, dispatch.getNodeSpec().rowId());
+ assertEquals(expPartId, dispatch.getNodeSpec().partitionId());
+ assertEquals("mycluster/search/cluster.mycluster/dispatchers/dispatch." + expDistributionKey, dispatch.getConfigId());
+ assertEquals(expPartId, getFdispatchrcConfig(dispatch).partition());
+ assertEquals(1, getFdispatchrcConfig(dispatch).dispatchlevel());
+
+ int numEngines = connectSpecs.length;
+ EngineAsserter ea = new EngineAsserter(numEngines, numEngines, dispatch);
+ for (int i = 0; i < numEngines; ++i) {
+ ea.assertEngine(0, i, connectSpecs[i]);
+ assertEquals(i, searchNodes[i].getNodeSpec().partitionId());
+ }
+ }
+
+ @Test
+ public void requireThatDispatchGroupsCanBeAutomaticallySetup() throws Exception {
+ ContentCluster cr = createCluster(getSimpleDispatchXml());
+ IndexedSearchCluster ix = cr.getSearch().getIndexed();
+ Dispatch tld = cr.getSearch().getIndexed().getTLDs().get(0);
+
+ assertEquals("tlds/tld.0", tld.getConfigId());
+ assertEquals(0, getFdispatchrcConfig(tld).dispatchlevel());
+ new EngineAsserter(2, 6, tld).
+ assertEngine(0, 0, "tcp/mh0:19113").
+ assertEngine(1, 0, "tcp/mh1:19113").
+ assertEngine(2, 0, "tcp/mh2:19113").
+ assertEngine(0, 1, "tcp/mh3:19113").
+ assertEngine(1, 1, "tcp/mh4:19113").
+ assertEngine(2, 1, "tcp/mh5:19113");
+
+ List<Dispatch> ds = getDispatchers(tld);
+ assertEquals(6, ds.size());
+ { // dispatch group 1
+ Dispatch[] dispatchers = {ds.get(0), ds.get(1), ds.get(2)};
+ String[] specs = {"tcp/mh0:19104", "tcp/mh1:19104", "tcp/mh2:19104"};
+ SearchNode[] searchNodes = {ix.getSearchNode(0), ix.getSearchNode(1), ix.getSearchNode(2)};
+ assertDispatchAndSearchNodes(0, dispatchers, specs, searchNodes);
+ }
+ { // dispatch group 2
+ Dispatch[] dispatchers = {ds.get(3), ds.get(4), ds.get(5)};
+ String[] specs = {"tcp/mh3:19104", "tcp/mh4:19104", "tcp/mh5:19104"};
+ SearchNode[] searchNodes = {ix.getSearchNode(3), ix.getSearchNode(4), ix.getSearchNode(5)};
+ assertDispatchAndSearchNodes(1, dispatchers, specs, searchNodes);
+ }
+ }
+
+ @Test
+ public void requireThatMaxHitsIsScaled() throws Exception {
+ ContentCluster cr = createCluster(getSimpleDispatchXml() + getMaxhitsTuning());
+ IndexedSearchCluster ix = cr.getSearch().getIndexed();
+ Dispatch tld = cr.getSearch().getIndexed().getTLDs().get(0);
+ PartitionsConfig.Builder builder = new PartitionsConfig.Builder();
+ tld.getConfig(builder);
+ PartitionsConfig config = new PartitionsConfig(builder);
+ assertThat(config.dataset().size(), is(1));
+ assertThat(config.dataset(0).maxhitspernode(), is(300));
+ for (Dispatch dispatch : getDispatchers(tld)) {
+ PartitionsConfig.Builder b = new PartitionsConfig.Builder();
+ dispatch.getConfig(b);
+ PartitionsConfig c= new PartitionsConfig(b);
+ assertThat(c.dataset().size(), is(1));
+ assertThat(c.dataset(0).maxhitspernode(), is(100));
+ }
+ }
+
+ private String getMaxhitsTuning() {
+ return "<tuning>" +
+ " <dispatch>" +
+ " <max-hits-per-partition>100</max-hits-per-partition>" +
+ " </dispatch>" +
+ "</tuning>";
+ }
+
+
+ @Test
+ public void requireThatSearchCoverageIsSetInMultilevelSetup() throws Exception {
+ ContentCluster cr = createCluster(getSimpleDispatchXml() + getCoverage());
+ Dispatch tld = cr.getSearch().getIndexed().getTLDs().get(0);
+ PartitionsConfig.Builder builder = new PartitionsConfig.Builder();
+ tld.getConfig(builder);
+ PartitionsConfig config = new PartitionsConfig(builder);
+ assertThat(config.dataset().size(), is(1));
+ assertEquals(95.0, config.dataset(0).minimal_searchcoverage(), 0.1);
+ for (Dispatch dispatch : getDispatchers(tld)) {
+ PartitionsConfig.Builder b = new PartitionsConfig.Builder();
+ dispatch.getConfig(b);
+ PartitionsConfig c= new PartitionsConfig(b);
+ assertThat(c.dataset().size(), is(1));
+ assertEquals(95.0, c.dataset(0).minimal_searchcoverage(), 0.1);
+ }
+ }
+
+ @Test
+ public void requireThatSearchCoverageIsSetInSingleLevelSetup() throws Exception {
+ TestRoot root = new TestDriver(true).buildModel(new MockApplicationPackage.Builder()
+ .withServices("<services version='1.0'>" +
+ "<content id='stateful' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents><document mode='index' type='music' /></documents>" +
+ " <nodes>" +
+ " <node distribution-key='1' hostalias='mockroot' />" +
+ " </nodes>" +
+ " <search><coverage><minimum>0.95</minimum></coverage></search>" +
+ "</content>" +
+ "<jdisc id='foo' version='1.0'>" +
+ " <search />" +
+ " <nodes><node hostalias='mockroot' /></nodes>" +
+ "</jdisc>" +
+ "</services>")
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build());
+ PartitionsConfig config = root.getConfig(PartitionsConfig.class, "stateful/search/cluster.stateful/tlds/foo.0.tld.0");
+ assertThat(config.dataset().size(), is(1));
+ assertEquals(95.0, config.dataset(0).minimal_searchcoverage(), 0.1);
+ }
+
+ private String getCoverage() {
+ return "<search>" +
+ " <coverage>" +
+ " <minimum>0.95</minimum>" +
+ " </coverage>" +
+ "</search>";
+ }
+
+ @Test
+ public void requireThatDispatchGroupsCanBeExplicitlySpecified() throws Exception {
+ ContentCluster cr = createCluster(getDispatchXml());
+ IndexedSearchCluster ix = cr.getSearch().getIndexed();
+ Dispatch tld = cr.getSearch().getIndexed().getTLDs().get(0);
+
+ assertEquals("tlds/tld.0", tld.getConfigId());
+ assertEquals(0, getFdispatchrcConfig(tld).dispatchlevel());
+ new EngineAsserter(2, 6, tld).
+ assertEngine(0, 0, "tcp/mh0:19113").
+ assertEngine(1, 0, "tcp/mh2:19113").
+ assertEngine(2, 0, "tcp/mh4:19113").
+ assertEngine(0, 1, "tcp/mh1:19113").
+ assertEngine(1, 1, "tcp/mh3:19113").
+ assertEngine(2, 1, "tcp/mh5:19113");
+
+ List<Dispatch> ds = getDispatchers(tld);
+ assertEquals(6, ds.size());
+ { // dispatch group 1
+ Dispatch[] dispatchers = {ds.get(0), ds.get(1), ds.get(2)};
+ String[] specs = {"tcp/mh0:19104", "tcp/mh2:19104", "tcp/mh4:19104"};
+ SearchNode[] searchNodes = {ix.getSearchNode(0), ix.getSearchNode(2), ix.getSearchNode(4)};
+ assertDispatchAndSearchNodes(0, dispatchers, specs, searchNodes);
+ }
+ { // dispatch group 2
+ Dispatch[] dispatchers = {ds.get(3), ds.get(4), ds.get(5)};
+ String[] specs = {"tcp/mh1:19104", "tcp/mh3:19104", "tcp/mh5:19104"};
+ SearchNode[] searchNodes = {ix.getSearchNode(1), ix.getSearchNode(3), ix.getSearchNode(5)};
+ assertDispatchAndSearchNodes(1, dispatchers, specs, searchNodes);
+ }
+ }
+
+ @Test
+ public void requireThatUnevenDispatchGroupsCanBeCreated() {
+ List<SearchNode> searchNodes = createSearchNodes(5);
+ List<DispatchSpec.Group> groups = DispatchGroupBuilder.createDispatchGroups(searchNodes, 3);
+ assertEquals(3, groups.size());
+ assertGroup(new int[]{0, 1}, groups.get(0));
+ assertGroup(new int[]{2, 3}, groups.get(1));
+ assertGroup(new int[]{4}, groups.get(2));
+ }
+
+ private List<SearchNode> createSearchNodes(int numNodes) {
+ List<SearchNode> searchNodes = new ArrayList<>();
+ MockRoot root = new MockRoot("");
+ for (int i = 0; i < numNodes; ++i) {
+ searchNodes.add(SearchNode.create(root, "mynode" + i, i, new NodeSpec(0, i), "mycluster", null, false));
+ }
+ return searchNodes;
+ }
+
+ private void assertGroup(int[] nodes, DispatchSpec.Group group) {
+ assertEquals(nodes.length, group.getNodes().size());
+ for (int i = 0; i < nodes.length; ++i) {
+ assertEquals(nodes[i], group.getNodes().get(i).getDistributionKey());
+ }
+ }
+
+ private ContentCluster createIllegalSetupWithMultipleNodeReferences() throws Exception {
+ String dispatchXml = " <dispatch>\n" +
+ " <group>\n" +
+ " <node distribution-key='10'/>\n" +
+ " <node distribution-key='11'/>\n" +
+ " <node distribution-key='12'/>\n" +
+ " </group>\n" +
+ " <group>\n" +
+ " <node distribution-key='12'/>\n" +
+ " <node distribution-key='13'/>\n" +
+ " <node distribution-key='14'/>\n" +
+ " </group>\n" +
+ " </dispatch>\n";
+ return createCluster(dispatchXml);
+ }
+
+ private ContentCluster createIllegalSetupWithMissingNodeReferences() throws Exception {
+ String dispatchXml = " <dispatch>\n" +
+ " <group>\n" +
+ " <node distribution-key='10'/>\n" +
+ " <node distribution-key='11'/>\n" +
+ " </group>\n" +
+ " <group>\n" +
+ " <node distribution-key='13'/>\n" +
+ " <node distribution-key='14'/>\n" +
+ " </group>\n" +
+ " </dispatch>\n";
+ return createCluster(dispatchXml);
+ }
+
+ private ContentCluster createIllegalSetupWithIllegalNodeReference() throws Exception {
+ String dispatchXml = " <dispatch>\n" +
+ " <group>\n" +
+ " <node distribution-key='10'/>\n" +
+ " <node distribution-key='11'/>\n" +
+ " <node distribution-key='12'/>\n" +
+ " </group>\n" +
+ " <group>\n" +
+ " <node distribution-key='13'/>\n" +
+ " <node distribution-key='14'/>\n" +
+ " <node distribution-key='15'/>\n" +
+ " <node distribution-key='19'/>\n" +
+ " </group>\n" +
+ " </dispatch>\n";
+ return createCluster(dispatchXml);
+ }
+
+ @Test
+ public void requireThatWeReferenceNodesOnlyOnceWhenSettingUpDispatchGroups() {
+ try {
+ createIllegalSetupWithMultipleNodeReferences();
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("node with distribution key '12' is referenced multiple times"));
+ }
+ }
+
+ @Test
+ public void requireThatWeReferenceAllNodesWhenSettingUpDispatchGroups() {
+ try {
+ createIllegalSetupWithMissingNodeReferences();
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("2 node(s) with distribution keys [12, 15] are not referenced"));
+ }
+ }
+
+ @Test
+ public void requireThatWeReferenceValidNodesWhenSettingUpDispatchGroups() throws Exception {
+ try {
+ createIllegalSetupWithIllegalNodeReference();
+ assertFalse("Did not get expected Exception", true);
+ } catch (Exception e) {
+ assertThat(e.getMessage(), containsString("node with distribution key '19' does not exists"));
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/TldTest.java b/config-model/src/test/java/com/yahoo/vespa/model/search/TldTest.java
new file mode 100644
index 00000000000..e3f4558076c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/search/TldTest.java
@@ -0,0 +1,152 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class TldTest {
+
+ @Test
+ public void requireThatServicesIsParsed() {
+ ApplicationPackage app = new MockApplicationPackage.Builder()
+ .withHosts("<hosts><host name='localhost'><alias>mockhost</alias></host><host name='my.other.host'><alias>mockhost2</alias></host></hosts>")
+ .withServices(
+ "<services>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='mockhost' />" +
+ " </admin>" +
+ " <jdisc version='1.0' id='default'>" +
+ " <search />" +
+ " <nodes>" +
+ " <node hostalias='mockhost'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content version='1.0' id='foo'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <group>" +
+ " <node hostalias='mockhost' distribution-key='0'/>" +
+ " <node hostalias='mockhost2' distribution-key='1'/>" +
+ " </group>" +
+ " <tuning>" +
+ " <dispatch>" +
+ " <max-hits-per-partition>69</max-hits-per-partition>" +
+ " <use-local-node>true</use-local-node>" +
+ " </dispatch>" +
+ " </tuning>" +
+ " </content>" +
+ "</services>")
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build();
+
+ PartitionsConfig.Builder builder = new PartitionsConfig.Builder();
+ new TestDriver(true).buildModel(app).getConfig(builder, "foo/search/cluster.foo/tlds/default.0.tld.0");
+ PartitionsConfig config = new PartitionsConfig(builder);
+
+ assertEquals(1, config.dataset().size());
+ assertEquals(69, config.dataset(0).maxhitspernode());
+ assertEquals(1, config.dataset(0).engine().size());
+ }
+
+ @Test
+ public void requireThatUseLocalPolicyIsOk() {
+ ApplicationPackage app = new MockApplicationPackage.Builder()
+ .withHosts(
+ "<hosts>" +
+ "<host name='search.node1'><alias>search1</alias></host>" +
+ "<host name='search.node2'><alias>search2</alias></host>" +
+ "<host name='jdisc.host.other'><alias>gateway</alias></host>" +
+ "</hosts>")
+ .withServices(
+ "<services>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='gateway' />" +
+ " </admin>" +
+ " <jdisc version='1.0' id='default'>" +
+ " <search />" +
+ " <nodes>" +
+ " <node hostalias='search1'/>" +
+ " <node hostalias='search2'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <jdisc version='1.0' id='gw'>" +
+ " <document-api/>" +
+ " <nodes>" +
+ " <node hostalias='gateway'/>" +
+ " </nodes>" +
+ " </jdisc>" +
+ " <content version='1.0' id='foo'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document type='music' mode='index'/>" +
+ " </documents>" +
+ " <group name='topGroup'>" +
+ " <distribution partitions='1|*'/>" +
+ " <group name='group1' distribution-key='0'>" +
+ " <node hostalias='search1' distribution-key='0'/>" +
+ " </group>" +
+ " <group name='group2' distribution-key='1'>" +
+ " <node hostalias='search2' distribution-key='1'/>" +
+ " </group>" +
+ " </group>" +
+ " <tuning>" +
+ " <dispatch>" +
+ " <use-local-node>true</use-local-node>" +
+ " </dispatch>" +
+ " </tuning>" +
+ " </content>" +
+ "</services>")
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build();
+
+ PartitionsConfig.Builder builder = new PartitionsConfig.Builder();
+ new TestDriver(true).buildModel(app).getConfig(builder, "foo/search/cluster.foo/tlds/gw.0.tld.0");
+ PartitionsConfig config = new PartitionsConfig(builder);
+
+ assertEquals(1, config.dataset().size());
+ //gateway TLD with no local search node gets all search nodes
+ assertEquals(2, config.dataset(0).engine().size());
+
+ assertEquals("rowid not equal 0",0,config.dataset(0).engine(0).rowid()); //Load Balance row 0
+ assertEquals("partid not equal 0",0,config.dataset(0).engine(0).partid());
+ assertTrue("Not configured with correct search node",config.dataset(0).engine(0).name_and_port().contains("search.node1"));
+
+ assertEquals("rowid not equal to 1",1,config.dataset(0).engine(1).rowid()); //Load Balance row 1
+ assertEquals("partid no equal to 0",0,config.dataset(0).engine(1).partid());
+ assertTrue("Not configured with correct search node",config.dataset(0).engine(1).name_and_port().contains("search.node2"));
+
+ //First container with a local search node
+ builder = new PartitionsConfig.Builder();
+ new TestDriver(true).buildModel(app).getConfig(builder, "foo/search/cluster.foo/tlds/default.0.tld.0");
+ config = new PartitionsConfig(builder);
+
+ assertEquals(1, config.dataset().size());
+ assertEquals(1, config.dataset(0).engine().size());
+ assertEquals(0,config.dataset(0).engine(0).rowid());
+ assertEquals(0,config.dataset(0).engine(0).partid());
+ assertTrue("Not configured with local search node as engine",config.dataset(0).engine(0).name_and_port().contains("search.node1"));
+
+ //Second container with a local search node
+ builder = new PartitionsConfig.Builder();
+ new TestDriver(true).buildModel(app).getConfig(builder, "foo/search/cluster.foo/tlds/default.1.tld.1");
+ config = new PartitionsConfig(builder);
+
+ assertEquals(1, config.dataset().size());
+ assertEquals(1, config.dataset(0).engine().size());
+ assertEquals(0,config.dataset(0).engine(0).rowid());
+ assertEquals(0,config.dataset(0).engine(0).partid());
+ assertTrue("Not configured with local search node as engine",config.dataset(0).engine(0).name_and_port().contains("search.node2"));
+
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java
new file mode 100644
index 00000000000..585c6fe0fb9
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java
@@ -0,0 +1,222 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search.test;
+
+import com.yahoo.vespa.config.search.IndexschemaConfig;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.config.search.RankProfilesConfig;
+import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
+import com.yahoo.search.config.IndexInfoConfig;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.config.search.AttributesConfig;
+import com.yahoo.vespa.configdefinition.IlscriptsConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.content.ContentSearchCluster;
+import com.yahoo.vespa.model.search.IndexedSearchCluster;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+// TODO: Author!
+public class DocumentDatabaseTestCase {
+
+ private String vespaHosts = "<?xml version='1.0' encoding='utf-8' ?>" +
+ "<hosts> " +
+ " <host name='foo'>" +
+ " <alias>node0</alias>" +
+ " </host>" +
+ "</hosts>";
+
+ private String createVespaServices(List<String> sdNames, String selection, String mode) {
+ StringBuilder retval = new StringBuilder();
+ retval.append("" +
+ "<?xml version='1.0' encoding='utf-8' ?>\n" +
+ "<services version='1.0'>\n" +
+ "<admin version='2.0'>\n" +
+ " <adminserver hostalias='node0' />\n" +
+ "</admin>\n" +
+ "<container version='1.0'>\n" +
+ " <nodes>\n" +
+ " <node hostalias='node0'/>\n" +
+ " </nodes>\n" +
+ " <search/>\n" +
+ "</container>\n" +
+ "<content version='1.0' id='test'>\n" +
+ " <redundancy>1</redundancy>\n");
+ retval.append(" <documents>\n");
+ for (String sdName : sdNames) {
+ retval.append("").append(" <document type='").append(sdName).append("' mode='" + mode + "'");
+ if (selection != null)
+ retval.append(" selection='").append(selection).append("'");
+ retval.append("/>\n");
+ }
+ retval.append(" </documents>\n");
+ retval.append("" +
+ " <nodes>\n" +
+ " <node hostalias='node0' distribution-key='0'/>\n" +
+ " </nodes>\n" +
+ "</content>\n" +
+ "</services>\n");
+ return retval.toString();
+ }
+
+ private ProtonConfig getProtonCfg(ContentSearchCluster cluster) {
+ ProtonConfig.Builder pb = new ProtonConfig.Builder();
+ cluster.getConfig(pb);
+ return new ProtonConfig(pb);
+ }
+
+ @Test
+ public void requireThatWeCanHaveOneSearchDefinition() throws IOException, SAXException, ParseException {
+ final List<String> sds = Arrays.asList("type1");
+ VespaModel model = new VespaModelCreatorWithMockPkg(vespaHosts, createVespaServices(sds, null, "index"), ApplicationPackageUtils.generateSearchDefinitions(sds)).create();
+ IndexedSearchCluster indexedSearchCluster = (IndexedSearchCluster)model.getSearchClusters().get(0);
+ ContentSearchCluster contentSearchCluster = model.getContentClusters().get("test").getSearch();
+ assertEquals(1, indexedSearchCluster.getDocumentDbs().size());
+ String type1Id = "test/search/cluster.test/type1";
+ ProtonConfig proton = getProtonCfg(contentSearchCluster);
+ assertEquals(1, proton.documentdb().size());
+ assertEquals("type1", proton.documentdb(0).inputdoctypename());
+ assertEquals(type1Id, proton.documentdb(0).configid());
+ ProtonConfig nodeCfg = getProtonCfg(contentSearchCluster);
+ assertEquals(1, nodeCfg.documentdb().size());
+ assertEquals("type1", nodeCfg.documentdb(0).inputdoctypename());
+ assertEquals(type1Id, nodeCfg.documentdb(0).configid());
+ }
+
+ private void assertDocTypeConfig(VespaModel model, String configId, String indexField, String attributeField) {
+ IndexschemaConfig icfg = model.getConfig(IndexschemaConfig.class, configId);
+ assertEquals(1, icfg.indexfield().size());
+ assertEquals(indexField, icfg.indexfield(0).name());
+ AttributesConfig acfg = model.getConfig(AttributesConfig.class, configId);
+ assertEquals(1, acfg.attribute().size());
+ assertEquals(attributeField, acfg.attribute(0).name());
+ RankProfilesConfig rcfg = model.getConfig(RankProfilesConfig.class, configId);
+ assertEquals(6, rcfg.rankprofile().size());
+ }
+
+ @Test
+ public void requireThatWeCanHaveMultipleSearchDefinitions() throws IOException, SAXException, ParseException {
+ final List<String> sds = Arrays.asList("type1", "type2", "type3");
+ VespaModel model = new VespaModelCreatorWithMockPkg(vespaHosts, createVespaServices(sds, null, "index"), ApplicationPackageUtils.generateSearchDefinitions(sds)).create();
+ IndexedSearchCluster indexedSearchCluster = (IndexedSearchCluster)model.getSearchClusters().get(0);
+ ContentSearchCluster contentSearchCluster = model.getContentClusters().get("test").getSearch();
+ String type1Id = "test/search/cluster.test/type1";
+ String type2Id = "test/search/cluster.test/type2";
+ String type3Id = "test/search/cluster.test/type3";
+ {
+ assertEquals(3, indexedSearchCluster.getDocumentDbs().size());
+ ProtonConfig proton = getProtonCfg(contentSearchCluster);
+ assertEquals(3, proton.documentdb().size());
+ assertEquals("type1", proton.documentdb(0).inputdoctypename());
+ assertEquals(type1Id, proton.documentdb(0).configid());
+ assertEquals("type2", proton.documentdb(1).inputdoctypename());
+ assertEquals(type2Id, proton.documentdb(1).configid());
+ assertEquals("type3", proton.documentdb(2).inputdoctypename());
+ assertEquals(type3Id, proton.documentdb(2).configid());
+ }
+ assertDocTypeConfig(model, type1Id, "f1", "f2");
+ assertDocTypeConfig(model, type2Id, "f3", "f4");
+ assertDocTypeConfig(model, type3Id, "f5", "f6");
+ {
+ IndexInfoConfig iicfg = model.getConfig(IndexInfoConfig.class, "test/search/cluster.test");
+ assertEquals(3, iicfg.indexinfo().size());
+ assertEquals("type1", iicfg.indexinfo().get(0).name());
+ assertEquals("type2", iicfg.indexinfo().get(1).name());
+ assertEquals("type3", iicfg.indexinfo().get(2).name());
+ }
+ {
+ AttributesConfig rac1 = model.getConfig(AttributesConfig.class, "test/search/cluster.test/type1");
+ assertEquals(1, rac1.attribute().size());
+ assertEquals("f2", rac1.attribute(0).name());
+ AttributesConfig rac2 = model.getConfig(AttributesConfig.class, "test/search/cluster.test/type2");
+ assertEquals(1, rac2.attribute().size());
+ assertEquals("f4", rac2.attribute(0).name());
+ }
+ {
+ IlscriptsConfig icfg = model.getConfig(IlscriptsConfig.class, "test/search/cluster.test");
+ assertEquals(3, icfg.ilscript().size());
+ assertEquals("type1", icfg.ilscript(0).doctype());
+ assertEquals("type2", icfg.ilscript(1).doctype());
+ assertEquals("type3", icfg.ilscript(2).doctype());
+ }
+ }
+
+ @Test
+ public void requireThatRelevantConfigIsAvailableForClusterSearcher() throws ParseException, IOException, SAXException {
+ final List<String> sds = Arrays.asList("type1", "type2");
+ VespaModel model = new VespaModelCreatorWithMockPkg(vespaHosts, createVespaServices(sds, null, "index"), ApplicationPackageUtils.generateSearchDefinitions(sds)).create();
+ String searcherId = "container/searchchains/chain/test/component/com.yahoo.prelude.cluster.ClusterSearcher";
+
+ { // documentdb-info config
+ DocumentdbInfoConfig dcfg = model.getConfig(DocumentdbInfoConfig.class, searcherId);
+ assertEquals(2, dcfg.documentdb().size());
+
+ { // type1
+ DocumentdbInfoConfig.Documentdb db = dcfg.documentdb(0);
+ assertEquals("type1", db.name());
+ assertEquals(6, db.rankprofile().size());
+
+ assertRankProfile(db, 0, "default", false, false);
+ assertRankProfile(db, 1, "unranked", false, false);
+ assertRankProfile(db, 2, "staticrank", false, false);
+ assertRankProfile(db, 3, "summaryfeatures", true, false);
+ assertRankProfile(db, 4, "inheritedsummaryfeatures", true, false);
+ assertRankProfile(db, 5, "rankfeatures", false, true);
+
+
+ assertEquals(2, db.summaryclass().size());
+ assertEquals("default", db.summaryclass(0).name());
+ assertEquals("attributeprefetch", db.summaryclass(1).name());
+ assertSummaryField(db, 0, 0, "f1", "longstring", true);
+ assertSummaryField(db, 0, 1, "f2", "integer", false);
+ }
+ { // type2
+ DocumentdbInfoConfig.Documentdb db = dcfg.documentdb(1);
+ assertEquals("type2", db.name());
+ }
+ }
+ { // attributes config
+ AttributesConfig acfg = model.getConfig(AttributesConfig.class, searcherId);
+ assertEquals(2, acfg.attribute().size());
+ assertEquals("f2", acfg.attribute(0).name());
+ assertEquals("f4", acfg.attribute(1).name());
+ assertEquals("f4", acfg.attribute(1).name());
+ }
+ }
+
+ private void assertRankProfile(DocumentdbInfoConfig.Documentdb db, int index, String name,
+ boolean hasSummaryFeatures, boolean hasRankFeatures) {
+ DocumentdbInfoConfig.Documentdb.Rankprofile rankProfile0 = db.rankprofile(index);
+ assertEquals(name, rankProfile0.name());
+ assertEquals(hasSummaryFeatures, rankProfile0.hasSummaryFeatures());
+ assertEquals(hasRankFeatures, rankProfile0.hasRankFeatures());
+ }
+
+ private void assertSummaryField(DocumentdbInfoConfig.Documentdb db, int summaryClassIndex, int fieldIndex,
+ String name, String type, boolean dynamic) {
+ DocumentdbInfoConfig.Documentdb.Summaryclass.Fields field = db.summaryclass(summaryClassIndex).fields(fieldIndex);
+ assertEquals(name, field.name());
+ assertEquals(type, field.type());
+ assertEquals(dynamic, field.dynamic());
+ }
+
+
+ @Test
+ public void requireThatConfigIsAvailableForStreaming() throws ParseException, IOException, SAXException {
+ final List<String> sds = Arrays.asList("type");
+ VespaModel model = new VespaModelCreatorWithMockPkg(vespaHosts, createVespaServices(sds, null, "streaming"), ApplicationPackageUtils.generateSearchDefinitions(sds)).create();
+
+ DocumentdbInfoConfig dcfg = model.getConfig(DocumentdbInfoConfig.class, "test/search/cluster.test.type");
+ assertEquals(1, dcfg.documentdb().size());
+ DocumentdbInfoConfig.Documentdb db = dcfg.documentdb(0);
+ assertEquals("type", db.name());
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentSelectionConverterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentSelectionConverterTest.java
new file mode 100644
index 00000000000..6941616acc8
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentSelectionConverterTest.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search.test;
+
+import com.yahoo.document.select.parser.ParseException;
+import com.yahoo.vespa.model.search.DocumentSelectionConverter;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for RemoveSelection.
+ * @author lulf
+ */
+public class DocumentSelectionConverterTest {
+ @Test
+ public void testQueryConversion() throws ParseException, IllegalArgumentException, UnsupportedOperationException {
+ DocumentSelectionConverter converter = new DocumentSelectionConverter("music.expire>now() - 3600 and video.expire > now() - 300");
+ assertEquals("expire:>now(3600)", converter.getQuery("music"));
+ assertEquals("expire:<now(3600)", converter.getInvertedQuery("music"));
+ assertEquals("expire:>now(300)", converter.getQuery("video"));
+ assertEquals("expire:<now(300)", converter.getInvertedQuery("video"));
+ assertTrue(null == converter.getQuery("book"));
+ assertTrue(null == converter.getInvertedQuery("book"));
+ }
+ @Test
+ public void testSelection() throws ParseException, IllegalArgumentException, UnsupportedOperationException {
+ DocumentSelectionConverter converter = new DocumentSelectionConverter("music.expire>music.expire.nowdate");
+ assertTrue(converter.getQuery("music") == null);
+ assertTrue(converter.getInvertedQuery("music") == null);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchClusterTest.java
new file mode 100644
index 00000000000..68deb96e632
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchClusterTest.java
@@ -0,0 +1,187 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search.test;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.container.QrSearchersConfig;
+import com.yahoo.document.DataType;
+import com.yahoo.search.config.ClusterConfig;
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.document.Attribute;
+import com.yahoo.searchdefinition.document.SDDocumentType;
+import com.yahoo.searchdefinition.document.SDField;
+import com.yahoo.vespa.indexinglanguage.expressions.AttributeExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.ScriptExpression;
+import com.yahoo.vespa.indexinglanguage.expressions.StatementExpression;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.test.utils.ApplicationPackageUtils;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * Unit tests for SearchCluster. Please use this instead of SearchModelTestCase if possible and
+ * write _unit_ tests. Thanks.
+ *
+ * @author <a href="musum@yahoo-inc.com">Harald Musum</a>
+ */
+public class SearchClusterTest {
+
+ private String vespaHosts = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<hosts> " +
+ "<host name=\"foo\">" +
+ "<alias>node0</alias>" +
+ "</host>" +
+ "<host name=\"bar\">" +
+ "<alias>node1</alias>" +
+ "</host>" +
+ "<host name=\"baz\">" +
+ "<alias>node2</alias>" +
+ "</host>" +
+ "</hosts>";
+
+ @Test
+ public void testSdConfigLogical() throws IOException, SAXException {
+ String services = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services version=\"1.0\">" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0' />" +
+ " </admin>" +
+ " <search version=\"2.0\">" +
+ " <qrservers>" +
+ " <qrserver hostalias=\"node0\" />" +
+ " </qrservers>" +
+ " <cluster name=\"s1\" indexingmode=\"realtime\">" +
+ " <searchdefinitions>" +
+ " <searchdefinition name=\"s1\" />" +
+ " <searchdefinition name=\"s2\" />" +
+ " </searchdefinitions>" +
+ " <documents selection=\"music\" feedname=\"a\" />" +
+ " <row index=\"0\">" +
+ " <searchnodes>" +
+ " <searchnode hostalias=\"node2\" index=\"0\" />" +
+ " </searchnodes>" +
+ " </row>" +
+ " </cluster>" +
+ " </search>" +
+ "</services>";
+ ApplicationPackage app = new VespaModelCreatorWithMockPkg(vespaHosts, services).appPkg;
+
+ // sd1
+ SDDocumentType sdt1=new SDDocumentType("s1");
+ Search search1 = new Search("s1", null);
+ SDField f1=new SDField("f1", DataType.STRING);
+ f1.addAttribute(new Attribute("f1", DataType.STRING));
+ f1.setIndexingScript(new ScriptExpression(new StatementExpression(new AttributeExpression("f1"))));
+ sdt1.addField(f1);
+ search1.addDocument(sdt1);
+
+ // sd2
+ SDDocumentType sdt2=new SDDocumentType("s2");
+ Search search2 = new Search("s2", null);
+ SDField f2=new SDField("f2", DataType.STRING);
+ f2.addAttribute(new Attribute("f2", DataType.STRING));
+ f2.setIndexingScript(new ScriptExpression(new StatementExpression(new AttributeExpression("f2"))));
+ sdt2.addField(f2);
+ search2.addDocument(sdt2);
+
+ SearchBuilder builder = new SearchBuilder();
+ builder.importRawSearch(search1);
+ builder.importRawSearch(search2);
+ builder.build();
+ }
+
+ @Test
+ public void search_model_is_connected_to_container_clusters_two_content_clusters() throws Exception {
+ String services = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services version=\"1.0\">" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0' />" +
+ " </admin>\n" +
+ " <jdisc version='1.0' id='j1'>\n" +
+ " <search>" +
+ " <chain id='s1Chain'>" +
+ " <searcher id='S1ClusterSearcher'/>" +
+ " </chain>" +
+ " <provider cluster='normal' id='normal' type='local'/>\n" +
+ " </search>" +
+ " <nodes>" +
+ " <node hostalias=\"node0\" />" +
+ " </nodes>" +
+ " </jdisc>" +
+
+ " <jdisc version='1.0' id='j2'>" +
+ " <search>" +
+ " <chain id='s2Chain'>" +
+ " <searcher id='S2ClusterSearcher'/>" +
+ " </chain>" +
+ " <provider cluster='xbulk' id='xbulk' type='local'/>" +
+ " </search>" +
+ " <nodes>" +
+ " <node hostalias=\"node2\" />" +
+ " </nodes>" +
+ " </jdisc>" +
+
+ " <content id='xbulk' version=\"1.0\">" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document mode='index' type=\"music\" />" +
+ " </documents>" +
+ " <nodes>" +
+ " <node hostalias=\"node0\" distribution-key=\"0\" />" +
+ " </nodes>" +
+ " </content>" +
+ " <content id=\"normal\" version='1.0'>" +
+ " <redundancy>2</redundancy>" +
+ " <documents>" +
+ " <document mode='index' type=\"music\" />" +
+ " </documents>" +
+ " <nodes>" +
+ " <node hostalias=\"node2\" distribution-key=\"0\" />" +
+ " </nodes>" +
+ " </content>" +
+ "</services>";
+
+ VespaModel model = new VespaModelCreatorWithMockPkg(vespaHosts, services, ApplicationPackageUtils.generateSearchDefinitions("music")).create();
+
+ ContainerCluster cluster1 = (ContainerCluster)model.getConfigProducer("j1").get();
+ assertFalse(cluster1.getSearch().getChains().localProviders().isEmpty());
+
+ ContainerCluster cluster2 = (ContainerCluster)model.getConfigProducer("j2").get();
+ assertFalse(cluster2.getSearch().getChains().localProviders().isEmpty());
+
+ QrSearchersConfig.Builder builder = new QrSearchersConfig.Builder();
+ cluster1.getConfig(builder);
+ QrSearchersConfig config = new QrSearchersConfig(builder);
+ System.out.println(config);
+
+ assertThat(config.searchcluster().size(), is(2));
+ int normalId = 0;
+ int bulkId = 1;
+ assertThat(config.searchcluster().get(normalId).name(), is("normal"));
+ assertThat(config.searchcluster().get(bulkId).name(), is("xbulk"));
+
+ ClusterConfig.Builder clusterConfigBuilder = new ClusterConfig.Builder();
+ model.getConfig(clusterConfigBuilder, "j1/searchchains/chain/normal/component/com.yahoo.prelude.cluster.ClusterSearcher");
+ ClusterConfig clusterConfig = new ClusterConfig(clusterConfigBuilder);
+ System.out.println(clusterConfig);
+ assertThat(clusterConfig.clusterId(), is(normalId));
+ assertThat(clusterConfig.clusterName(), is("normal"));
+
+ ClusterConfig.Builder clusterConfigBuilder2 = new ClusterConfig.Builder();
+ model.getConfig(clusterConfigBuilder2, "j2/searchchains/chain/xbulk/component/com.yahoo.prelude.cluster.ClusterSearcher");
+ ClusterConfig clusterConfig2 = new ClusterConfig(clusterConfigBuilder2);
+ System.out.println(clusterConfig2);
+ assertThat(clusterConfig2.clusterId(), is(bulkId));
+ assertThat(clusterConfig2.clusterName(), is("xbulk"));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchNodeTest.java b/config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchNodeTest.java
new file mode 100644
index 00000000000..e9aa8f2267f
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/search/test/SearchNodeTest.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search.test;
+
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.config.search.core.ProtonConfig;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.Host;
+import com.yahoo.vespa.model.HostResource;
+import com.yahoo.vespa.model.search.NodeSpec;
+import com.yahoo.vespa.model.search.SearchNode;
+import com.yahoo.vespa.model.search.TransactionLogServer;
+import org.hamcrest.CoreMatchers;
+import org.junit.Assert;
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
+
+/**
+ * Unit tests for search node.
+ *
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class SearchNodeTest {
+
+ private void assertBaseDir(String expected, SearchNode node) {
+ ProtonConfig.Builder builder = new ProtonConfig.Builder();
+ node.getConfig(builder);
+ ProtonConfig cfg = new ProtonConfig(builder);
+ assertEquals(expected, cfg.basedir());
+ }
+
+ private void prepare(MockRoot root, SearchNode node) {
+ Host host = new Host(root, "mockhost");
+ TransactionLogServer tls = new TransactionLogServer(root, "mycluster");
+ tls.setHostResource(new HostResource(host));
+ tls.setBasePort(100);
+ tls.initService();
+ node.setTls(tls);
+ node.setHostResource(new HostResource(host));
+ node.setBasePort(200);
+ node.initService();
+ root.freezeModelTopology();
+ }
+
+ @Test
+ public void requireThatBasedirIsCorrectForElasticMode() {
+ MockRoot root = new MockRoot("");
+ SearchNode node = SearchNode.create(root, "mynode", 3, new NodeSpec(7, 5), "mycluster", null, false);
+ prepare(root, node);
+ assertBaseDir(Defaults.getDefaults().vespaHome() + "var/db/vespa/search/cluster.mycluster/n3", node);
+ }
+
+ @Test
+ public void requireThatPreShutdownCommandIsEmptyWhenNotActivated() {
+ MockRoot root = new MockRoot("");
+ SearchNode node = SearchNode.create(root, "mynode", 3, new NodeSpec(7, 5), "mycluster", null, false);
+ node.setHostResource(new HostResource(new Host(node, "mynbode")));
+ node.initService();
+ assertFalse(node.getPreShutdownCommand().isPresent());
+ }
+
+ @Test
+ public void requireThatPreShutdownCommandUsesPrepareRestartWhenActivated() {
+ MockRoot root = new MockRoot("");
+ SearchNode node = SearchNode.create(root, "mynode2", 4, new NodeSpec(7, 5), "mycluster", null, true);
+ node.setHostResource(new HostResource(new Host(node, "mynbode2")));
+ node.initService();
+ assertTrue(node.getPreShutdownCommand().isPresent());
+ Assert.assertThat(node.getPreShutdownCommand().get(),
+ CoreMatchers.containsString("vespa-proton-cmd " + node.getRpcPort() + " prepareRestart"));
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/utils/DispatchUtils.java b/config-model/src/test/java/com/yahoo/vespa/model/search/utils/DispatchUtils.java
new file mode 100644
index 00000000000..558fd4f9a80
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/search/utils/DispatchUtils.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.search.utils;
+
+import com.yahoo.vespa.config.search.core.FdispatchrcConfig;
+import com.yahoo.vespa.config.search.core.PartitionsConfig;
+import com.yahoo.vespa.model.search.Dispatch;
+
+import static org.junit.Assert.assertEquals;
+
+public class DispatchUtils {
+
+ public static PartitionsConfig.Dataset getDataset(Dispatch dispatch) {
+ PartitionsConfig.Builder builder = new PartitionsConfig.Builder();
+ dispatch.getConfig(builder);
+ PartitionsConfig cfg = new PartitionsConfig(builder);
+ assertEquals(1, cfg.dataset().size());
+ return cfg.dataset(0);
+ }
+
+ public static FdispatchrcConfig getFdispatchrcConfig(Dispatch dispatch) {
+ FdispatchrcConfig.Builder builder = new FdispatchrcConfig.Builder();
+ dispatch.getConfig(builder);
+ return new FdispatchrcConfig(builder);
+ }
+
+ public static void assertEngine(int rowId, int partitionId, PartitionsConfig.Dataset.Engine engine) {
+ assertEquals(rowId, engine.rowid());
+ assertEquals(partitionId, engine.partid());
+ }
+
+ public static void assertEngine(int rowId, int partitionId, String connectSpec, PartitionsConfig.Dataset.Engine engine) {
+ assertEngine(rowId, partitionId, engine);
+ assertEquals(connectSpec, engine.name_and_port());
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/storage/DistributionBitCalculatorTest.java b/config-model/src/test/java/com/yahoo/vespa/model/storage/DistributionBitCalculatorTest.java
new file mode 100644
index 00000000000..6d6c31ac0d2
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/storage/DistributionBitCalculatorTest.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.storage;
+
+import com.yahoo.vespa.model.content.DistributionBitCalculator;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class DistributionBitCalculatorTest {
+
+ @Test
+ public void testBitCalculator() {
+ ContentCluster.DistributionMode mode = ContentCluster.DistributionMode.STRICT;
+ assertEquals(8, DistributionBitCalculator.getDistributionBits(1, mode));
+ assertEquals(16, DistributionBitCalculator.getDistributionBits(10, mode));
+ assertEquals(21, DistributionBitCalculator.getDistributionBits(100, mode));
+ assertEquals(25, DistributionBitCalculator.getDistributionBits(500, mode));
+ assertEquals(28, DistributionBitCalculator.getDistributionBits(1000, mode));
+
+ mode = ContentCluster.DistributionMode.LOOSE;
+ assertEquals( 8, DistributionBitCalculator.getDistributionBits(1, mode));
+ assertEquals( 8, DistributionBitCalculator.getDistributionBits(4, mode));
+ assertEquals(16, DistributionBitCalculator.getDistributionBits(5, mode));
+ assertEquals(16, DistributionBitCalculator.getDistributionBits(199, mode));
+ assertEquals(24, DistributionBitCalculator.getDistributionBits(200, mode));
+ assertEquals(24, DistributionBitCalculator.getDistributionBits(2500, mode));
+
+ mode = ContentCluster.DistributionMode.LEGACY;
+ assertEquals( 1, DistributionBitCalculator.getDistributionBits(1, mode));
+ assertEquals(14, DistributionBitCalculator.getDistributionBits(4, mode));
+ assertEquals(19, DistributionBitCalculator.getDistributionBits(16, mode));
+ assertEquals(23, DistributionBitCalculator.getDistributionBits(200, mode));
+ assertEquals(28, DistributionBitCalculator.getDistributionBits(2500, mode));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/storage/test/StorageModelTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/storage/test/StorageModelTestCase.java
new file mode 100644
index 00000000000..d7e9286b854
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/storage/test/StorageModelTestCase.java
@@ -0,0 +1,56 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.storage.test;
+
+import com.yahoo.metrics.MetricsmanagerConfig;
+import com.yahoo.vespa.config.content.FleetcontrollerConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.content.cluster.ContentCluster;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Tests storage model
+ *
+ *
+ * @author gjoranv
+ */
+public class StorageModelTestCase {
+
+ @Test(expected=RuntimeException.class)
+ public void testTwoClustersSameName() throws Exception {
+ createModel("src/test/cfg/storage/twoclusterssamename");
+ }
+
+ private VespaModel createModel(String filename) {
+ return new VespaModelCreatorWithFilePkg(filename).create();
+ }
+
+ @Test
+ public void testIndexGreaterThanNumNodes() throws Exception {
+ VespaModel vespaModel = createModel("src/test/cfg/storage/app_index_higher_than_num_nodes");
+
+ // Test fleet controller config
+ FleetcontrollerConfig fleetController1Config = new FleetcontrollerConfig((FleetcontrollerConfig.Builder)
+ vespaModel.getConfig(new FleetcontrollerConfig.Builder(), "content/fleetcontroller"));
+
+ assertEquals(60000, fleetController1Config.storage_transition_time());
+ assertEquals(8, fleetController1Config.ideal_distribution_bits());
+ }
+
+ @Test
+ public void testMetricsSnapshotIntervalYAMAS() throws Exception {
+ VespaModel vespaModel = createModel("src/test/cfg/storage/clustercontroller_advanced");
+ ContentCluster contentCluster = vespaModel.getContentClusters().values().iterator().next();
+ assertNotNull(contentCluster);
+ MetricsmanagerConfig.Builder builder = new MetricsmanagerConfig.Builder();
+ contentCluster.getConfig(builder);
+ MetricsmanagerConfig config = new MetricsmanagerConfig(builder);
+ assertThat(config.snapshot().periods(0), is(60));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/ApiConfigModel.java b/config-model/src/test/java/com/yahoo/vespa/model/test/ApiConfigModel.java
new file mode 100644
index 00000000000..af9dc275922
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/ApiConfigModel.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelRepo;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.vespa.model.builder.xml.dom.LegacyConfigModelBuilder;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This is a plugin for testing the plugin API exchange mechanism in
+ * the vespamodel. It uses the API of another plugin.
+ *
+ * @author gjoranv
+ */
+public class ApiConfigModel extends ConfigModel {
+
+ private List<ApiService> apiServices = new ArrayList<>();
+
+ public ApiConfigModel(ConfigModelContext modelContext) {
+ super(modelContext);
+ }
+
+ // Inherit doc from ConfigModel.
+ public void prepare(ConfigModelRepo configModelRepo) {
+ int numSimpleServices = 0;
+ ConfigModel simplePlugin = configModelRepo.get("simple");
+
+ if ((simplePlugin != null) && (simplePlugin instanceof TestApi)) {
+ TestApi testApi = (TestApi) simplePlugin;
+ numSimpleServices = testApi.getNumSimpleServices();
+ }
+ for (Object apiService : apiServices) {
+ ApiService as = (ApiService) apiService;
+ as.setNumSimpleServices(numSimpleServices);
+ }
+ }
+
+ public static class Builder extends LegacyConfigModelBuilder<ApiConfigModel> {
+
+ public Builder() {
+ super(ApiConfigModel.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return Arrays.asList(ConfigModelId.fromName("api"));
+ }
+
+ @Override
+ public void doBuild(ApiConfigModel configModel, Element spec, ConfigModelContext modelContext) {
+ NodeList pl = spec.getElementsByTagName("apiservice");
+ if (pl.getLength() > 0) {
+ for (int i=0; i < pl.getLength(); i++) {
+ configModel.apiServices.add(new DomTestServiceBuilder.ApiServiceBuilder(i).build(modelContext.getParentProducer(),
+ (Element) pl.item(i)));
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/ApiService.java b/config-model/src/test/java/com/yahoo/vespa/model/test/ApiService.java
new file mode 100644
index 00000000000..e0756fec650
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/ApiService.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+
+/**
+ * This is a service for testing the plugin exchange mechanism in the
+ * vespamodel. It provides some data that are made public in the API
+ * of the plugin that owns it.
+ *
+ * @author gjoranv
+ */
+public class ApiService extends AbstractService implements com.yahoo.test.StandardConfig.Producer {
+
+ private int numSimpleServices = 0;
+
+ /**
+ * Creates a new ApiService instance
+ *
+ * @param parent The parent ConfigProducer.
+ * @param name Service name
+ */
+ public ApiService(AbstractConfigProducer<?> parent, String name) {
+ super(parent, name);
+ }
+
+ public void getConfig(com.yahoo.test.StandardConfig.Builder builder) {
+ builder.astring("apiservice");
+
+ }
+
+ public void setNumSimpleServices(int nss) {
+ numSimpleServices = nss;
+ }
+
+ public int getPortCount() { return 0; }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/DomTestServiceBuilder.java b/config-model/src/test/java/com/yahoo/vespa/model/test/DomTestServiceBuilder.java
new file mode 100644
index 00000000000..02a1318fa56
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/DomTestServiceBuilder.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.builder.xml.dom.VespaDomBuilder;
+import org.w3c.dom.Element;
+
+/**
+ * Builders for test services
+ */
+public class DomTestServiceBuilder {
+
+
+ static class SimpleServiceBuilder
+ extends VespaDomBuilder.DomConfigProducerBuilder<SimpleService> {
+ int i;
+
+ public SimpleServiceBuilder(int i) {
+ this.i = i;
+ }
+
+ @Override
+ protected SimpleService doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new SimpleService(parent, "simpleservice." + i);
+ }
+ }
+
+ static class ApiServiceBuilder
+ extends VespaDomBuilder.DomConfigProducerBuilder<ApiService> {
+ int i;
+
+ public ApiServiceBuilder(int i) {
+ this.i = i;
+ }
+
+ @Override
+ protected ApiService doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new ApiService(parent, "apiservice." + i);
+ }
+ }
+
+ static class ParentServiceBuilder
+ extends VespaDomBuilder.DomConfigProducerBuilder<ParentService> {
+ int i;
+
+ public ParentServiceBuilder(int i) {
+ this.i = i;
+ }
+
+ @Override
+ protected ParentService doBuild(AbstractConfigProducer parent,
+ Element spec) {
+ return new ParentService(parent, "parentservice." + i, spec);
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/ModelAmendingTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/test/ModelAmendingTestCase.java
new file mode 100644
index 00000000000..1032f5099c6
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/ModelAmendingTestCase.java
@@ -0,0 +1,168 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.ConfigModelRegistry;
+import com.yahoo.config.model.MapConfigModelRegistry;
+import com.yahoo.config.model.builder.xml.ConfigModelBuilder;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.builder.xml.dom.DomContentBuilder;
+import com.yahoo.vespa.model.container.ContainerCluster;
+import com.yahoo.vespa.model.container.ContainerModel;
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilder;
+import com.yahoo.vespa.model.content.Content;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Demonstrates how a model can be added at build time to amend another model.
+ * This is useful is situations where the core Vespa config models needs to be
+ * modified by third party code which follows the installed environment rather than
+ * the application.
+ *
+ * @author bratseth
+ */
+public class ModelAmendingTestCase {
+
+ @Test
+ public void testModelAmending() throws IOException, SAXException {
+ ConfigModelRegistry amendingModelRepo = MapConfigModelRegistry.createFromList(new ContainerModelAmenderBuilder(),
+ new ContentModelAmenderBuilder());
+ VespaModel model = new VespaModel(new MockApplicationPackage.Builder()
+ .withServices(
+ "<services version='1.0'>" +
+ " <jdisc id='test1' version='1.0'>" +
+ " <search />" +
+ " </jdisc>" +
+ " <jdisc id='test2' version='1.0'>" +
+ " <http><server id='server1' port='19107'/></http>" +
+ " <document-api/>" +
+ " </jdisc>" +
+ " <content id='test3' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document mode='index' type='testtype1'/>" +
+ " </documents>" +
+ " </content>" +
+ " <content id='test4' version='1.0'>" +
+ " <redundancy>1</redundancy>" +
+ " <documents>" +
+ " <document mode='index' type='testtype1'/>" +
+ " </documents>" +
+ " </content>" +
+ "</services>")
+ .withSearchDefinitions(
+ searchDefinition("testtype1"))
+ .build(),
+ amendingModelRepo);
+ assertEquals(1, model.getHostSystem().getHosts().size());
+
+ // Check that explicit jdisc clusters are amended
+ assertEquals(4, model.getContainerClusters().size());
+ assertNotNull(model.getContainerClusters().get("test1").getComponentsMap().get(new ComponentId("com.yahoo.MyAmendedComponent")));
+ assertNotNull(model.getContainerClusters().get("test2").getComponentsMap().get(new ComponentId("com.yahoo.MyAmendedComponent")));
+ assertNotNull(model.getContainerClusters().get("cluster.test3.indexing").getComponentsMap().get(new ComponentId("com.yahoo.MyAmendedComponent")));
+ assertNotNull(model.getContainerClusters().get("cluster.test4.indexing").getComponentsMap().get(new ComponentId("com.yahoo.MyAmendedComponent")));
+ }
+
+ private List<String> searchDefinition(String name) {
+ return Collections.singletonList(
+ "search " + name + " {" +
+ " document " + name + " {" +
+ " field testfield type string {}" +
+ " }" +
+ "}");
+ }
+
+ public static class ContainerModelAmenderBuilder extends ConfigModelBuilder<ContainerModelAmender> {
+
+ private boolean built = false;
+
+ public ContainerModelAmenderBuilder() {
+ super(ContainerModelAmender.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return ContainerModelBuilder.configModelIds;
+ }
+
+ @Override
+ public void doBuild(ContainerModelAmender model, Element spec, ConfigModelContext modelContext) {
+ if (built) return; // the same instance will be called once per jdisc cluster
+ for (ContainerModel containerModel : model.containerModels)
+ amend(containerModel.getCluster());
+ built = true;
+ }
+
+ static void amend(ContainerCluster cluster) {
+ cluster.addSimpleComponent("com.yahoo.MyAmendedComponent", null, "my-amendment-bundle");
+ }
+
+ }
+
+ public static class ContainerModelAmender extends ConfigModel {
+
+ /** The container models this builder amends */
+ private final Collection<ContainerModel> containerModels;
+
+ public ContainerModelAmender(ConfigModelContext modelContext, Collection<ContainerModel> containerModels) {
+ super(modelContext);
+ this.containerModels = containerModels;
+ }
+
+ @Override
+ public boolean isServing() { return false; }
+
+ }
+
+ public static class ContentModelAmenderBuilder extends ConfigModelBuilder<ContentModelAmender> {
+
+ private boolean built = false;
+
+ public ContentModelAmenderBuilder() {
+ super(ContentModelAmender.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return DomContentBuilder.configModelIds;
+ }
+
+ @Override
+ public void doBuild(ContentModelAmender model, Element spec, ConfigModelContext modelContext) {
+ if (built) return; // the same instance will be called once per content cluster
+ for (Content contentModel : model.contentModels)
+ contentModel.ownedIndexingCluster().ifPresent(ContainerModelAmenderBuilder::amend);
+ built = true;
+ }
+ }
+
+ public static class ContentModelAmender extends ConfigModel {
+
+ private final Collection<Content> contentModels;
+
+ public ContentModelAmender(ConfigModelContext modelContext, Collection<Content> contentModels) {
+ super(modelContext);
+ this.contentModels = contentModels;
+ }
+
+ @Override
+ public boolean isServing() { return false; }
+
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/ModelConfigProviderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/test/ModelConfigProviderTest.java
new file mode 100644
index 00000000000..1e5a5255321
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/ModelConfigProviderTest.java
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.cloud.config.ModelConfig;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test HostSystem
+ *
+ * @author musum
+ */
+public class ModelConfigProviderTest {
+
+ /**
+ * Get the config via ConfigInstance based API, by getting whole config
+ */
+ @Test
+ public void testGetModelConfig() {
+ VespaModel vespaModel = new VespaModelCreatorWithFilePkg("src/test/cfg/admin/adminconfig20").create();
+ ModelConfig config = vespaModel.getConfig(ModelConfig.class, "");
+ assertEquals(config.hosts().size(), 1);
+ ModelConfig.Hosts localhost = config.hosts(0); //Actually set to hostname.
+ int numLogservers=0;
+ int numSlobroks=0;
+ for (ModelConfig.Hosts.Services service : localhost.services()) {
+ if ("logserver".equals(service.type())) {
+ numLogservers++;
+ }
+ if ("slobrok".equals(service.type())) {
+ numSlobroks++;
+ }
+ }
+ assertEquals(1, numLogservers);
+ assertEquals(2, numSlobroks);
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/ParentService.java b/config-model/src/test/java/com/yahoo/vespa/model/test/ParentService.java
new file mode 100644
index 00000000000..f1ec51a1200
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/ParentService.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.test.StandardConfig.Builder;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * This is a service that creates child services
+ */
+public class ParentService extends AbstractService implements com.yahoo.test.StandardConfig.Producer {
+
+ public int childCnt = 0;
+
+ /**
+ * Creates a new ParentService instance
+ *
+ * @param parent The parent ConfigProducer.
+ * @param name Service name
+ * @param config The xml config Element for this Service
+ */
+ public ParentService(AbstractConfigProducer parent, String name,
+ Element config)
+ {
+ super(parent, name);
+
+ int s,p; s=p=0;
+ NodeList childNodes = config.getChildNodes();
+ for (int i=0; i < childNodes.getLength(); i++) {
+ Node child = childNodes.item(i);
+ if (! (child instanceof Element)) {
+ // skip #text and #comment nodes
+ continue;
+ }
+ Element e = (Element)child;
+ String service = e.getTagName();
+
+ if (service.equals("simpleservice")) {
+ new SimpleService(this, "simpleservice."+s);
+ s++;
+ }
+ else if (service.equals("parentservice")) {
+ new ParentService(this, "parentservice."+p, e);
+ p++;
+ }
+ else {
+ throw new IllegalArgumentException("Unknown service: " + service);
+ }
+ }
+ }
+
+ @Override
+ public void getConfig(Builder builder) {
+ builder.astring("parentservice");
+ }
+
+ public int getPortCount() { return 0; }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/PortsMetaTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/test/PortsMetaTestCase.java
new file mode 100644
index 00000000000..40dc134190b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/PortsMetaTestCase.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.vespa.model.PortsMeta;
+
+/**
+ * Tests proper functioning of the PortsMeta.
+ *
+ *
+ * @author Vidar Larsen
+ */
+public class PortsMetaTestCase extends junit.framework.TestCase {
+ public void testRegister() throws Exception {
+ PortsMeta pm = new PortsMeta();
+ pm.on(0).tag("foo");
+ pm.on(1).tag("bar");
+ pm.on(5).tag("xyzzy");
+
+ assertTrue(pm.contains(0, "foo"));
+ assertTrue(pm.contains(1, "bar"));
+ assertTrue(pm.contains(5, "xyzzy"));
+ assertFalse(pm.contains(0, "bar"));
+ assertFalse(pm.contains(2, "anything"));
+ }
+ public void testAdminStatusApi() throws Exception {
+ PortsMeta pm = new PortsMeta()
+ .on(0).tag("rpc").tag("nc").tag("admin").tag("status")
+ .on(1).tag("rpc").tag("rtx").tag("admin").tag("status")
+ .on(2).tag("http").tag("admin");
+
+ assertEquals(1, pm.getRpcAdminOffset().intValue());
+ assertEquals(1, pm.getRpcStatusOffset().intValue());
+ assertEquals(2, pm.getHttpAdminOffset().intValue());
+ assertNull(pm.getHttpStatusOffset());
+ }
+
+} \ No newline at end of file
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/SimpleConfigModel.java b/config-model/src/test/java/com/yahoo/vespa/model/test/SimpleConfigModel.java
new file mode 100644
index 00000000000..ca1cc6a3500
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/SimpleConfigModel.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.config.model.ConfigModel;
+import com.yahoo.config.model.ConfigModelContext;
+import com.yahoo.config.model.builder.xml.ConfigModelId;
+import com.yahoo.vespa.model.builder.xml.dom.LegacyConfigModelBuilder;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A simple test config model.
+ *
+ * @author gjoranv
+ */
+public class SimpleConfigModel extends ConfigModel implements TestApi {
+
+ private List<SimpleService> simpleServices = new ArrayList<>();
+ private List<ParentService> parentServices = new ArrayList<>();
+
+ public SimpleConfigModel(ConfigModelContext modelContext) {
+ super(modelContext);
+ }
+
+ /** Implement TestApi */
+ public int getNumSimpleServices() {
+ return simpleServices.size();
+ }
+ public int getNumParentServices() {
+ return parentServices.size();
+ }
+
+ public static class Builder extends LegacyConfigModelBuilder<SimpleConfigModel> {
+
+ public Builder() {
+ super(SimpleConfigModel.class);
+ }
+
+ @Override
+ public List<ConfigModelId> handlesElements() {
+ return Arrays.asList(ConfigModelId.fromName("simple"));
+ }
+
+ @Override
+ public void doBuild(SimpleConfigModel configModel, Element spec, ConfigModelContext modelContext) {
+ int s,p; s=p=0;
+
+ // Validate the services given in the config
+ NodeList childNodes = spec.getChildNodes();
+ for (int i=0; i < childNodes.getLength(); i++) {
+ Node child = childNodes.item(i);
+ if (! (child instanceof Element)) {
+ // skip #text and #comment nodes
+ continue;
+ }
+ Element e = (Element)child;
+ String service = e.getTagName();
+
+ if (service.equals("simpleservice")) {
+ configModel.simpleServices.add(new DomTestServiceBuilder.SimpleServiceBuilder(s).build(modelContext.getParentProducer(), e));
+ s++;
+ }
+ else if (service.equals("parentservice")) {
+ configModel.parentServices.add(new DomTestServiceBuilder.ParentServiceBuilder(p).build(modelContext.getParentProducer(), e));
+ p++;
+ }
+ else {
+ throw new IllegalArgumentException("Unknown service: " + service);
+ }
+ }
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/SimpleService.java b/config-model/src/test/java/com/yahoo/vespa/model/test/SimpleService.java
new file mode 100644
index 00000000000..cfef392f2c4
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/SimpleService.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.test.StandardConfig.Builder;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.vespa.model.AbstractService;
+
+import java.util.HashMap;
+
+/**
+ * This service has a desired default port and returns the actual
+ * baseport from getConfig().
+ *
+ * @author gjoranv
+ */
+public class SimpleService extends AbstractService implements com.yahoo.test.StandardConfig.Producer {
+
+ /**
+ * Creates a new SimpleService instance
+ *
+ * @param parent The parent ConfigProducer.
+ * @param name Service name
+ */
+ public SimpleService(AbstractConfigProducer parent, String name) {
+ super(parent, name);
+ portsMeta.on(0).tag("base")
+ .on(1).tag("base")
+ .on(2).tag("base")
+ .on(3).tag("base")
+ .on(4).tag("base");
+ }
+
+ @Override
+ public void getConfig(Builder builder) {
+ builder.astring("simpleservice").baseport(getRelativePort(0));
+ }
+
+ public int getWantedPort(){ return 10000; }
+ public int getPortCount() { return 5; }
+
+ // Make sure this service is listed in the sentinel config
+ public String getStartupCommand() { return "sleep 0"; }
+
+ public boolean getAutostartFlag() { return false; }
+ public boolean getAutorestartFlag() { return false; }
+
+ @Override
+ public HashMap<String,String> getDefaultMetricDimensions(){
+ HashMap<String, String> dimensions = new HashMap<>();
+ dimensions.put("clustername", "testClusterName");
+ return dimensions;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/TestApi.java b/config-model/src/test/java/com/yahoo/vespa/model/test/TestApi.java
new file mode 100644
index 00000000000..b0218769e25
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/TestApi.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+/**
+ * This is a simple API for testing the plugin api exchange mechanism.
+ *
+ * @author gjoranv
+ */
+public interface TestApi {
+ public int getNumSimpleServices();
+ public int getNumParentServices();
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTestCase.java
new file mode 100644
index 00000000000..a86dc68d9dc
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/VespaModelTestCase.java
@@ -0,0 +1,341 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test;
+
+import com.yahoo.cloud.config.ApplicationIdConfig;
+import com.yahoo.cloud.config.SlobroksConfig;
+import com.yahoo.cloud.config.ZookeepersConfig;
+import com.yahoo.cloud.config.log.LogdConfig;
+import com.yahoo.collections.Pair;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.deploy.DeployProperties;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.provision.HostsXmlProvisioner;
+import com.yahoo.config.model.provision.InMemoryProvisioner;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.config.model.test.TestDriver;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ProvisionInfo;
+import com.yahoo.container.core.ContainerHttpConfig;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.messagebus.MessagebusConfig;
+import com.yahoo.net.HostName;
+import com.yahoo.net.LinuxInetAddress;
+import com.yahoo.vespa.config.ConfigKey;
+import com.yahoo.vespa.config.ConfigPayload;
+import com.yahoo.vespa.config.ConfigPayloadBuilder;
+import com.yahoo.vespa.config.UnknownConfigIdException;
+import com.yahoo.vespa.config.buildergen.ConfigDefinition;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.model.ConfigProducer;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.admin.Admin;
+import com.yahoo.vespa.model.admin.Configserver;
+import com.yahoo.vespa.model.application.validation.Validation;
+import com.yahoo.vespa.model.test.utils.CommonVespaModelSetup;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithFilePkg;
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author gjoranv
+ */
+public class VespaModelTestCase {
+
+ private static final String TESTDIR = "src/test/cfg/application/";
+ private static final String simpleHosts = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<hosts> " +
+ "<host name=\"localhost\">" +
+ "<alias>node0</alias>" +
+ "</host>" +
+ "</hosts>";
+
+ public static VespaModel getVespaModel(String configPath) {
+ return getVespaModel(configPath, true);
+ }
+
+ public static VespaModel getVespaModel(String configPath, boolean validateXml) {
+ VespaModelCreatorWithFilePkg creator = new VespaModelCreatorWithFilePkg(configPath);
+ return creator.create(validateXml);
+ }
+
+ // Debugging
+ @SuppressWarnings({"UnusedDeclaration"})
+ private static void dumpTree(ConfigProducer producer) {
+ Map<String, ? extends ConfigProducer> id2cp = producer.getChildren();
+ for (ConfigProducer c : id2cp.values()) {
+ System.out.println("id: " + c.getConfigId());
+ if (c.getChildren().size() > 0) {
+ dumpTree(c);
+ }
+ }
+ }
+
+ // Verify that common config from plugins is delivered from the root node for any configId, using the Builder based API
+ @Test
+ public void testCommonConfig() throws Exception {
+ VespaModel model = getVespaModel(TESTDIR + "app_nohosts/");
+ LogdConfig.Builder b = new LogdConfig.Builder();
+ b = (LogdConfig.Builder) model.getConfig(b, "");
+ LogdConfig c = new LogdConfig(b);
+ assertEquals(c.logserver().host(), LinuxInetAddress.getLocalHost().getCanonicalHostName());
+
+ SlobroksConfig.Builder sb = new SlobroksConfig.Builder();
+ sb = (com.yahoo.cloud.config.SlobroksConfig.Builder) model.getConfig(sb, "");
+ SlobroksConfig sbc = new SlobroksConfig(sb);
+ assertEquals(sbc.slobrok().size(), 1);
+
+ ZookeepersConfig.Builder zb = new ZookeepersConfig.Builder();
+ zb = (ZookeepersConfig.Builder) model.getConfig(zb, "");
+ ZookeepersConfig zc = new ZookeepersConfig(zb);
+ assertEquals(zc.zookeeperserverlist().split(",").length, 2);
+ assertTrue(zc.zookeeperserverlist().startsWith(LinuxInetAddress.getLocalHost().getCanonicalHostName()));
+
+ ApplicationIdConfig.Builder appIdBuilder = new ApplicationIdConfig.Builder();
+ appIdBuilder = (ApplicationIdConfig.Builder) model.getConfig(appIdBuilder, "");
+ ApplicationIdConfig applicationIdConfig = new ApplicationIdConfig(appIdBuilder);
+ assertEquals(ApplicationId.defaultId().tenant().value(), applicationIdConfig.tenant());
+ assertEquals(ApplicationId.defaultId().application().value(), applicationIdConfig.application());
+ assertEquals(ApplicationId.defaultId().instance().value(), applicationIdConfig.instance());
+ }
+
+ @Test
+ public void testHostsConfig() {
+ VespaModel model = getVespaModel(TESTDIR + "app_qrserverandgw");
+ LogdConfig config = getLogdConfig(model, "");
+ assertEquals(config.logserver().host(), HostName.getLocalhost());
+ assertNotNull(config);
+ config = getLogdConfig(model, "hosts");
+ assertNotNull(config);
+ assertEquals(config.logserver().host(), HostName.getLocalhost());
+ }
+
+ private static LogdConfig getLogdConfig(VespaModel model, String configId) {
+ LogdConfig.Builder b = new LogdConfig.Builder();
+ b = (LogdConfig.Builder) model.getConfig(b, configId);
+ if (b == null)
+ return null;
+ return new LogdConfig(b);
+ }
+
+ @Test
+ public void testHostsOverrides() throws IOException, SAXException {
+ VespaModel model = new VespaModelCreatorWithMockPkg(
+ simpleHosts,
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services version=\"1.0\">" +
+ "<config name=\"cloud.config.log.logd\">" +
+ "<logserver><host>foo</host></logserver>" +
+ "</config>" +
+ "<admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node0\" />" +
+ "</admin>" +
+ "</services>").create();
+ LogdConfig config = getLogdConfig(model, "");
+ assertNotNull(config);
+ assertEquals(config.logserver().host(), "foo");
+ config = getLogdConfig(model, "hosts/" + HostName.getLocalhost() + "/logd");
+ assertNotNull(config);
+ assertEquals(config.logserver().host(), "foo");
+ }
+
+ @Ignore
+ @Test(expected = UnknownConfigIdException.class)
+ public void testIllegalConfigIdWithBuilders() {
+ VespaModel model = getVespaModel(TESTDIR + "app_nohosts/");
+ DocumentmanagerConfig.Builder db = new DocumentmanagerConfig.Builder();
+ model.getConfig(db, "bogus");
+ }
+
+ @Test
+ public void testConfigLists() {
+ VespaModel model = getVespaModel(TESTDIR + "app_nohosts/");
+ assertTrue(model.allConfigsProduced().size() > 0);
+ assertTrue(model.allConfigIds().size() > 0);
+ }
+
+ @Test
+ public void testCreateFromReaders() throws SAXException, IOException {
+ VespaModel model = CommonVespaModelSetup.createVespaModelWithMusic(
+ simpleHosts,
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services version=\"1.0\">" +
+ "<admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node0\" />" +
+ "</admin>" +
+ "<container version=\"1.0\">" +
+ " <nodes>" +
+ " <node hostalias=\"node0\" />" +
+ " </nodes>" +
+ " <search/>" +
+ " <document-api/>" +
+ "</container>" +
+ "<content id=\"music\" version=\"1.0\">" +
+ " <redundancy>1</redundancy>" +
+ " <nodes>" +
+ " <node hostalias=\"node0\" distribution-key=\"0\"/>" +
+ " </nodes>" +
+ " <documents>" +
+ " <document type=\"music\" mode=\"index\"/>" +
+ " </documents>" +
+ "</content>" +
+ "</services>");
+ ContainerHttpConfig container = new ContainerHttpConfig((ContainerHttpConfig.Builder) model.getConfig(new ContainerHttpConfig.Builder(), "container/container.0"));
+ assertEquals(container.port().search(), Defaults.getDefaults().vespaWebServicePort());
+ MessagebusConfig.Builder mBusB = new MessagebusConfig.Builder();
+ model.getConfig(mBusB, "client");
+ MessagebusConfig mBus = new MessagebusConfig(mBusB);
+ assertEquals(mBus.routingtable().size(), 1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testHostsWithoutAliases() {
+ new TestDriver().buildModel(
+ "<services version='1.0'>" +
+ " <admin version='2.0'>" +
+ " <adminserver hostalias='node0' />" +
+ " </admin>" +
+ "</services>",
+ "<hosts>" +
+ " <host name='localhost'>" +
+ " <alias>node0</alias>" +
+ " </host>" +
+ " <host name='foo.yahoo.com' />" +
+ "</hosts>");
+ }
+
+ class MyLogger implements DeployLogger {
+ List<Pair<Level, String>> msgs = new ArrayList<>();
+ @Override
+ public void log(Level level, String message) {
+ msgs.add(new Pair<>(level, message));
+ }
+ }
+
+ @Test
+ public void testDeployLogger() throws IOException, SAXException {
+ final String services = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services version=\"1.0\">" +
+ "<config name=\"unknsownfoo\">" +
+ "<logserver><host>foo</host></logserver>" +
+ "</config>" +
+ "<admin version=\"2.0\">" +
+ " <adminserver hostalias=\"node0\" />" +
+ "</admin>" +
+ "</services>";
+
+ MyLogger logger = new MyLogger();
+ final DeployState.Builder builder = new DeployState.Builder();
+ builder.modelHostProvisioner(new HostsXmlProvisioner(new StringReader(simpleHosts)));
+ ApplicationPackage app = new MockApplicationPackage.Builder()
+ .withHosts(simpleHosts)
+ .withServices(services)
+ .build();
+ DeployState deployState = builder.deployLogger(logger).applicationPackage(app).build();
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+ Validation.validate(model, true, deployState);
+ System.out.println(logger.msgs);
+ assertFalse(logger.msgs.isEmpty());
+ }
+
+ @Test
+ public void testNoAdmin() throws IOException, SAXException {
+ VespaModel model = CommonVespaModelSetup.createVespaModelWithMusic(
+ simpleHosts,
+ "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
+ "<services version=\"1.0\">" +
+ "</services>");
+ Admin admin = model.getAdmin();
+ assertThat(admin.getSlobroks().size(), is(1));
+ assertThat(admin.getConfigservers().size(), is(1));
+ Set<HostInfo> hosts = model.getHosts();
+ assertThat(hosts.size(), is(1));
+ //logd, config proxy, sentinel, config server, slobrok, log server, file distributor
+ HostInfo host = hosts.iterator().next();
+ assertThat(host.getServices().size(), is(7));
+ new LogdConfig((LogdConfig.Builder) model.getConfig(new LogdConfig.Builder(), "admin/model"));
+
+ }
+
+ @Test
+ public void testNoMultitenantHostExported() throws IOException, SAXException {
+ ApplicationPackage applicationPackage = new MockApplicationPackage.Builder()
+ .withServices("<services version='1.0'><admin version='3.0'><nodes count='1' /></admin></services>")
+ .build();
+ DeployState deployState = new DeployState.Builder()
+ .applicationPackage(applicationPackage)
+ .modelHostProvisioner(new InMemoryProvisioner(true, "host1.yahoo.com"))
+ .properties(new DeployProperties.Builder()
+ .configServerSpecs(Arrays.asList(new Configserver.Spec("cfghost", 1234, 1235, 1236)))
+ .multitenant(true)
+ .build())
+ .build();
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+ ProvisionInfo info = model.getProvisionInfo().get();
+ assertEquals("Admin version 3 is ignored, and there are no other hosts to borrow for admin services", 0, info.getHosts().size());
+ }
+
+ @Test
+ public void testMinimalApp() throws IOException, SAXException {
+ VespaModel model = new VespaModel(new MockApplicationPackage.Builder()
+ .withServices("<services version='1.0'><jdisc version='1.0'><search /></jdisc></services>")
+ .build());
+ assertThat(model.getHostSystem().getHosts().size(), is(1));
+ assertThat(model.getContainerClusters().size(), is(1));
+ }
+
+ @Test
+ public void testPermanentServices() throws IOException, SAXException {
+ ApplicationPackage app = MockApplicationPackage.createEmpty();
+ DeployState.Builder builder = new DeployState.Builder().applicationPackage(app);
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), builder.build());
+ assertThat(model.getContainerClusters().size(), is(0));
+ model = new VespaModel(new NullConfigModelRegistry(), builder.permanentApplicationPackage(Optional.of(FilesApplicationPackage.fromFile(new File(TESTDIR, "app_permanent")))).build());
+ assertThat(model.getContainerClusters().size(), is(1));
+ }
+
+ @Test
+ public void testConfigResolving() throws IOException {
+ VespaModel model = VespaModelTestCase.getVespaModel(TESTDIR + "app_nohosts/");
+ ConfigDefinition def = new ConfigDefinition(LogdConfig.CONFIG_DEF_NAME, LogdConfig.CONFIG_DEF_SCHEMA);
+ ConfigKey<?> key = new ConfigKey<>(LogdConfig.CONFIG_DEF_NAME, "", LogdConfig.CONFIG_DEF_NAMESPACE);
+ ConfigPayload payload = model.getConfig(key, def, null);
+ assertPort(payload, 19081);
+
+ ConfigPayloadBuilder builder = new ConfigPayloadBuilder();
+ builder.getObject("logserver").setField("port", "19082");
+ payload = model.getConfig(key, def, ConfigPayload.fromBuilder(builder));
+ assertPort(payload, 19082);
+ payload = model.getConfig(key, def, ConfigPayload.fromBuilder(builder));
+ assertPort(payload, 19082);
+ }
+
+ private void assertPort(ConfigPayload payload, long expectedPort) {
+ long port = payload.getSlime().get().field("logserver").field("port").asLong();
+ assertThat(port, is(expectedPort));
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/utils/ApplicationPackageUtils.java b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/ApplicationPackageUtils.java
new file mode 100644
index 00000000000..387ab07a685
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/ApplicationPackageUtils.java
@@ -0,0 +1,94 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test.utils;
+
+import com.yahoo.searchdefinition.Search;
+import com.yahoo.searchdefinition.SearchBuilder;
+import com.yahoo.searchdefinition.parser.ParseException;
+import com.yahoo.vespa.model.search.SearchDefinition;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * For testing purposes only.
+ * @author <a href="mailto:geirst@yahoo-inc.com">Geir Storli</a>
+ */
+public class ApplicationPackageUtils {
+
+ public static String generateSearchDefinition(String name, String field1, String field2) {
+ String sd = "" +
+ "search " + name + "{" +
+ " document " + name + "{" +
+ " field " + field1 + " type string {\n" +
+ " indexing: index | summary\n" +
+ " summary: dynamic\n" +
+ " header\n" +
+ " }\n" +
+ " field " + field2 + " type int {\n" +
+ " indexing: attribute | summary\n" +
+ " header\n" +
+ " }\n" +
+ " }\n" +
+ " rank-profile staticrank inherits default {" +
+ " first-phase { expression: attribute(" + field2 + ") }" +
+ " }" +
+ " rank-profile summaryfeatures inherits default {" +
+ " first-phase { expression: attribute(" + field2 + ") }\n" +
+ " summary-features: attribute(" + field2 + ")" +
+ " }" +
+ " rank-profile inheritedsummaryfeatures inherits summaryfeatures {" +
+ " }" +
+ " rank-profile rankfeatures {" +
+ " first-phase { expression: attribute(" + field2 + ") }\n" +
+ " rank-features: attribute(" + field2 + ")" +
+ " }" +
+ "}";
+ return sd;
+ }
+
+ public static Search createSearch(String name, String field1, String field2) throws ParseException {
+ SearchBuilder sb = new SearchBuilder();
+ sb.importString(generateSearchDefinition(name, field1, field2));
+ sb.build();
+ return sb.getSearch();
+ }
+
+ public static SearchDefinition createSearchDefinition(String name, String field1, String field2) throws ParseException {
+ com.yahoo.searchdefinition.Search type = ApplicationPackageUtils.createSearch(name, field1, field2);
+ return new SearchDefinition(type.getName(), type);
+ }
+
+ public static List<String> generateSearchDefinition(String name) {
+ return generateSearchDefinitions(name);
+ }
+
+ public static List<String> generateSearchDefinitions(String ... sdNames) {
+ return generateSearchDefinitions(Arrays.asList(sdNames));
+ }
+
+ public static List<SearchDefinition> createSearchDefinition(String name) throws ParseException {
+ return createSearchDefinitions(Arrays.asList(name));
+ }
+
+ public static List<SearchDefinition> createSearchDefinitions(List<String> sdNames) throws ParseException {
+ List<SearchDefinition> sds = new ArrayList<>();
+ int i = 0;
+ for (String sdName : sdNames) {
+ sds.add(createSearchDefinition(sdName, "f" + (i + 1), "f" + (i + 2)));
+ i = i + 2;
+ }
+ return sds;
+ }
+
+ public static List<String> generateSearchDefinitions(List<String> sdNames) {
+ List<String> sds = new ArrayList<>();
+ int i = 0;
+ for (String sdName : sdNames) {
+ sds.add(generateSearchDefinition(sdName, "f" + (i + 1), "f" + (i + 2)));
+ i = i + 2;
+ }
+ return sds;
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/utils/CommonVespaModelSetup.java b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/CommonVespaModelSetup.java
new file mode 100644
index 00000000000..b8388e15e0c
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/CommonVespaModelSetup.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test.utils;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.model.VespaModel;
+
+import java.io.File;
+
+/**
+ * @author tonytv
+ */
+//TODO Remove, use VespaModelCreatorWithMockPkg or VespaModelCreatorWithFilePkg instead
+public class CommonVespaModelSetup {
+
+ public static VespaModel createVespaModelWithMusic(String path) {
+ return createVespaModelWithMusic(new File(path));
+ }
+
+ public static VespaModel createVespaModelWithMusic(File dir) {
+ VespaModelCreatorWithFilePkg modelCreator = new VespaModelCreatorWithFilePkg(dir);
+ return modelCreator.create();
+ }
+
+ public static VespaModel createVespaModelWithMusic(String hosts, String services) {
+ ApplicationPackage app = new MockApplicationPackage.Builder()
+ .withHosts(hosts)
+ .withServices(services)
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build();
+ VespaModelCreatorWithMockPkg modelCreator = new VespaModelCreatorWithMockPkg(app);
+ return modelCreator.create();
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/utils/DeployLoggerStub.java b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/DeployLoggerStub.java
new file mode 100644
index 00000000000..31999a9ae51
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/DeployLoggerStub.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test.utils;
+
+
+import com.yahoo.config.application.api.DeployLogger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * A logger stub that stores the log output to a list.
+ *
+ * @author bjorncs
+ */
+public class DeployLoggerStub implements DeployLogger {
+
+ public final List<LogEntry> entries = new ArrayList<>();
+
+ @Override
+ public void log(Level level, String message) {
+ entries.add(new LogEntry(level, message));
+ }
+
+ public static class LogEntry {
+ public final Level level;
+ public final String message;
+
+ public LogEntry(Level level, String message) {
+ this.level = level;
+ this.message = message;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof LogEntry)) return false;
+
+ LogEntry logEntry = (LogEntry) o;
+
+ if (!level.equals(logEntry.level)) return false;
+ if (!message.equals(logEntry.message)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = level.hashCode();
+ result = 31 * result + message.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "level='" + level + ", message='" + message + "'";
+ }
+ }
+
+ public LogEntry getLast() {
+ return entries.get(entries.size() - 1);
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithFilePkg.java b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithFilePkg.java
new file mode 100644
index 00000000000..14216733d2a
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithFilePkg.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test.utils;
+
+import com.yahoo.config.model.ConfigModelRegistry;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.application.provider.*;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.Validation;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * For testing purposes only
+ *
+ * @author tonytv
+ */
+public class VespaModelCreatorWithFilePkg {
+
+ private FilesApplicationPackage applicationPkg;
+
+ private ConfigModelRegistry configModelRegistry;
+
+ public VespaModelCreatorWithFilePkg(String directoryName) {
+ this(new File(directoryName));
+ }
+
+ public VespaModelCreatorWithFilePkg(File directory) {
+ this(directory, new NullConfigModelRegistry());
+ }
+
+ public VespaModelCreatorWithFilePkg(String directoryName, ConfigModelRegistry configModelRegistry) {
+ this(new File(directoryName), configModelRegistry);
+ }
+
+ public VespaModelCreatorWithFilePkg(File directory, ConfigModelRegistry configModelRegistry) {
+ this.configModelRegistry = configModelRegistry;
+ this.applicationPkg = FilesApplicationPackage.fromFile(directory);
+ }
+
+ public VespaModel create() {
+ return create(true);
+ }
+
+ public void validate() throws IOException {
+ ApplicationPackageXmlFilesValidator.createTestXmlValidator(applicationPkg.getAppDir()).checkApplication();
+ ApplicationPackageXmlFilesValidator.checkIncludedDirs(applicationPkg);
+ }
+
+ public VespaModel create(boolean validateApplicationWithSchema) {
+ try {
+ if (validateApplicationWithSchema) {
+ validate();
+ }
+ DeployState deployState = new DeployState.Builder().applicationPackage(applicationPkg).build();
+ VespaModel model = new VespaModel(configModelRegistry, deployState);
+ // Validate, but without checking configSources or routing (routing
+ // is constructed in a special way and cannot always be validated in
+ // this step for unit tests)
+ Validation.validate(model, false, false, deployState);
+ return model;
+ } catch (Exception e) {
+ throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithMockPkg.java b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithMockPkg.java
new file mode 100644
index 00000000000..42d9f874c0b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/test/utils/VespaModelCreatorWithMockPkg.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.test.utils;
+
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.api.ConfigChangeAction;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.application.provider.SchemaValidator;
+import com.yahoo.config.model.test.MockApplicationPackage;
+import com.yahoo.vespa.model.VespaModel;
+import com.yahoo.vespa.model.application.validation.Validation;
+
+import java.util.List;
+
+/**
+ * For testing purposes only.
+ *
+ * @author tonytv
+ */
+public class VespaModelCreatorWithMockPkg {
+
+ public final ApplicationPackage appPkg;
+ public DeployState deployState = null;
+ public List<ConfigChangeAction> configChangeActions;
+
+ public VespaModelCreatorWithMockPkg(String hosts, String services) {
+ this(new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).build());
+ }
+
+ public VespaModelCreatorWithMockPkg(String hosts, String services, List<String> searchDefinitions) {
+ this(new MockApplicationPackage.Builder().withHosts(hosts).withServices(services).withSearchDefinitions(searchDefinitions).build());
+ }
+
+ public VespaModelCreatorWithMockPkg(ApplicationPackage appPkg) {
+ this.appPkg = appPkg;
+ }
+
+ public VespaModel create() {
+ DeployState deployState = new DeployState.Builder().applicationPackage(appPkg).build();
+ return create(true, deployState);
+ }
+
+ public VespaModel create(DeployState.Builder deployStateBuilder) {
+ return create(true, deployStateBuilder.applicationPackage(appPkg).build());
+ }
+
+ public VespaModel create(boolean validate, DeployState deployState) {
+ try {
+ this.deployState = deployState;
+ VespaModel model = new VespaModel(new NullConfigModelRegistry(), deployState);
+ if (validate) {
+ try {
+ SchemaValidator validator = SchemaValidator.createTestValidatorHosts();
+ if (appPkg.getHosts() != null) {
+ validator.validate(appPkg.getHosts());
+ }
+ validator = SchemaValidator.createTestValidatorServices();
+ validator.validate(appPkg.getServices());
+ } catch (Exception e) {
+ System.err.println(e.getClass());
+ throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
+ }
+ // Validate, but without checking configSources or routing (routing
+ // is constructed in a special way and cannot always be validated in
+ // this step for unit tests)
+ configChangeActions = Validation.validate(model, false, false, deployState);
+ }
+ return model;
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/utils/DurationTest.java b/config-model/src/test/java/com/yahoo/vespa/model/utils/DurationTest.java
new file mode 100644
index 00000000000..5d056142f63
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/utils/DurationTest.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.utils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class DurationTest {
+ @Test
+ public void testDurationUnits() {
+ assertEquals(1000, new Duration("1").getMilliSeconds());
+ assertEquals(2.0, new Duration("2").getSeconds(), 0.0001);
+ assertEquals(1, new Duration("1ms").getMilliSeconds());
+ assertEquals(2000, new Duration("2s").getMilliSeconds());
+ assertEquals(5 * 60 * 1000, new Duration("5m").getMilliSeconds());
+ assertEquals(3 * 60 * 60 * 1000, new Duration("3h").getMilliSeconds());
+ assertEquals(24 * 60 * 60 * 1000, new Duration("1d").getMilliSeconds());
+
+ assertEquals(1400, new Duration("1.4s").getMilliSeconds());
+ assertEquals(1400, new Duration("1.4 s").getMilliSeconds());
+ }
+
+ private void assertException(String str) {
+ try {
+ new Duration(str);
+ fail("Exception not thrown for string: " + str);
+ } catch (Exception e) {
+ }
+ }
+
+ @Test
+ public void testParseError() {
+ assertException("bjarne");
+ assertException("");
+ assertException("1 foo");
+ assertException("1.5 bar");
+ assertException("-5");
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/utils/FileSenderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/utils/FileSenderTest.java
new file mode 100644
index 00000000000..41fc2edc3a7
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/utils/FileSenderTest.java
@@ -0,0 +1,176 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.utils;
+
+import com.yahoo.config.FileNode;
+import com.yahoo.config.FileReference;
+import com.yahoo.config.model.application.provider.BaseDeployLogger;
+import com.yahoo.config.model.producer.AbstractConfigProducer;
+import com.yahoo.config.model.producer.UserConfigRepo;
+import com.yahoo.config.model.test.MockRoot;
+import com.yahoo.vespa.config.*;
+import com.yahoo.vespa.model.AbstractService;
+import com.yahoo.vespa.model.SimpleConfigProducer;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class FileSenderTest {
+
+ private SimpleConfigProducer<?> producer;
+ private ConfigPayloadBuilder builder;
+ private List<AbstractService> serviceList;
+ private ConfigDefinition def;
+ private TestService service;
+
+ @Before
+ public void setup() {
+ MockRoot root = new MockRoot();
+ producer = new SimpleConfigProducer<>(root, "test");
+ service = new TestService(root, "service");
+ serviceList = new ArrayList<>();
+ serviceList.add(service);
+ ConfigDefinitionKey key = new ConfigDefinitionKey("myname", "mynamespace");
+ def = new ConfigDefinition("myname", "1", "mynamespace");
+ builder = new ConfigPayloadBuilder(def, new ArrayList<String>());
+ Map<ConfigDefinitionKey, ConfigPayloadBuilder> builderMap = new HashMap<>();
+ builderMap.put(key, builder);
+ UserConfigRepo testRepo = new UserConfigRepo(builderMap);
+ producer.setUserConfigs(testRepo);
+ }
+
+ @Test
+ public void require_that_simple_file_fields_are_modified() {
+ def.addFileDef("fileVal");
+ def.addStringDef("stringVal");
+ builder.setField("fileVal", "foo.txt");
+ builder.setField("stringVal", "foo.txt");
+ service.pathToRef.put("foo.txt", new FileNode("fooshash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ assertThat(builder.getObject("fileVal").getValue(), is("fooshash"));
+ assertThat(builder.getObject("stringVal").getValue(), is("foo.txt"));
+ }
+
+ @Test
+ public void require_that_simple_path_fields_are_modified() {
+ def.addPathDef("fileVal");
+ def.addStringDef("stringVal");
+ builder.setField("fileVal", "foo.txt");
+ builder.setField("stringVal", "foo.txt");
+ service.pathToRef.put("foo.txt", new FileNode("fooshash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ assertThat(builder.getObject("fileVal").getValue(), is("fooshash"));
+ assertThat(builder.getObject("stringVal").getValue(), is("foo.txt"));
+ }
+
+ @Test
+ public void require_that_fields_in_inner_arrays_are_modified() {
+ def.innerArrayDef("inner").addFileDef("fileVal");
+ def.innerArrayDef("inner").addStringDef("stringVal");
+ ConfigPayloadBuilder inner = builder.getArray("inner").append();
+ inner.setField("fileVal", "bar.txt");
+ inner.setField("stringVal", "bar.txt");
+ service.pathToRef.put("bar.txt", new FileNode("barhash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ assertThat(builder.getArray("inner").get(0).getObject("fileVal").getValue(), is("barhash"));
+ assertThat(builder.getArray("inner").get(0).getObject("stringVal").getValue(), is("bar.txt"));
+ }
+
+ @Test
+ public void require_that_arrays_are_modified() {
+ def.arrayDef("fileArray").setTypeSpec(new ConfigDefinition.TypeSpec("fileArray", "file", null, null, null, null));
+ def.arrayDef("pathArray").setTypeSpec(new ConfigDefinition.TypeSpec("pathArray", "path", null, null, null, null));
+ def.arrayDef("stringArray").setTypeSpec(new ConfigDefinition.TypeSpec("stringArray", "string", null, null, null, null));
+ builder.getArray("fileArray").append("foo.txt");
+ builder.getArray("fileArray").append("bar.txt");
+ builder.getArray("pathArray").append("path.txt");
+ builder.getArray("stringArray").append("foo.txt");
+ service.pathToRef.put("foo.txt", new FileNode("foohash").value());
+ service.pathToRef.put("bar.txt", new FileNode("barhash").value());
+ service.pathToRef.put("path.txt", new FileNode("pathhash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ assertThat(builder.getArray("fileArray").get(0).getValue(), is("foohash"));
+ assertThat(builder.getArray("fileArray").get(1).getValue(), is("barhash"));
+ assertThat(builder.getArray("pathArray").get(0).getValue(), is("pathhash"));
+ assertThat(builder.getArray("stringArray").get(0).getValue(), is("foo.txt"));
+ }
+
+ @Test
+ public void require_that_structs_are_modified() {
+ def.structDef("struct").addFileDef("fileVal");
+ def.structDef("struct").addStringDef("stringVal");
+ builder.getObject("struct").setField("fileVal", "foo.txt");
+ builder.getObject("struct").setField("stringVal", "foo.txt");
+ service.pathToRef.put("foo.txt", new FileNode("foohash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ assertThat(builder.getObject("struct").getObject("fileVal").getValue(), is("foohash"));
+ assertThat(builder.getObject("struct").getObject("stringVal").getValue(), is("foo.txt"));
+ }
+
+ @Test
+ public void require_that_leaf_maps_are_modified() {
+ def.leafMapDef("fileMap").setTypeSpec(new ConfigDefinition.TypeSpec("fileMap", "file", null, null, null, null));
+ def.leafMapDef("pathMap").setTypeSpec(new ConfigDefinition.TypeSpec("pathMap", "path", null, null, null, null));
+ def.leafMapDef("stringMap").setTypeSpec(new ConfigDefinition.TypeSpec("stringMap", "string", null, null, null, null));
+ builder.getMap("fileMap").put("foo", "foo.txt");
+ builder.getMap("fileMap").put("bar", "bar.txt");
+ builder.getMap("pathMap").put("path", "path.txt");
+ builder.getMap("stringMap").put("bar", "bar.txt");
+ service.pathToRef.put("foo.txt", new FileNode("foohash").value());
+ service.pathToRef.put("bar.txt", new FileNode("barhash").value());
+ service.pathToRef.put("path.txt", new FileNode("pathhash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ assertThat(builder.getMap("fileMap").get("foo").getValue(), is("foohash"));
+ assertThat(builder.getMap("fileMap").get("bar").getValue(), is("barhash"));
+ assertThat(builder.getMap("pathMap").get("path").getValue(), is("pathhash"));
+ assertThat(builder.getMap("stringMap").get("bar").getValue(), is("bar.txt"));
+ }
+
+ @Test
+ public void require_that_fields_in_inner_maps_are_modified() {
+ def.structMapDef("inner").addFileDef("fileVal");
+ def.structMapDef("inner").addStringDef("stringVal");
+ ConfigPayloadBuilder inner = builder.getMap("inner").put("foo");
+ inner.setField("fileVal", "bar.txt");
+ inner.setField("stringVal", "bar.txt");
+ service.pathToRef.put("bar.txt", new FileNode("barhash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ assertThat(builder.getMap("inner").get("foo").getObject("fileVal").getValue(), is("barhash"));
+ assertThat(builder.getMap("inner").get("foo").getObject("stringVal").getValue(), is("bar.txt"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void require_that_null_files_are_not_sent() {
+ def.addFileDef("fileVal");
+ service.pathToRef.put("foo.txt", new FileNode("fooshash").value());
+ FileSender.sendUserConfiguredFiles(producer, serviceList, new BaseDeployLogger());
+ }
+
+
+ private static class TestService extends AbstractService {
+ public Map<String, FileReference> pathToRef = new HashMap<>();
+ public TestService(AbstractConfigProducer<?> parent, String name) {
+ super(parent, name);
+ }
+
+ @Override
+ public FileReference sendFile(String relativePath) {
+ return pathToRef.get(relativePath);
+ }
+
+ @Override
+ public int getPortCount() {
+ return 0;
+ }
+ }
+}
diff --git a/config-model/src/test/java/com/yahoo/vespa/model/utils/internal/ReflectionUtilTest.java b/config-model/src/test/java/com/yahoo/vespa/model/utils/internal/ReflectionUtilTest.java
new file mode 100644
index 00000000000..55ed49ed97b
--- /dev/null
+++ b/config-model/src/test/java/com/yahoo/vespa/model/utils/internal/ReflectionUtilTest.java
@@ -0,0 +1,91 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.utils.internal;
+
+import com.yahoo.test.ArraytypesConfig;
+import com.yahoo.config.ChangesRequiringRestart;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.test.SimpletypesConfig;
+import com.yahoo.vespa.config.ConfigKey;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author lulf
+ * @author bjorncs
+ * @since 5.1
+ */
+public class ReflectionUtilTest {
+
+ private static interface ComplexInterface extends SimpletypesConfig.Producer, ArraytypesConfig.Producer {
+ }
+
+ private static class SimpleProducer implements SimpletypesConfig.Producer {
+ @Override
+ public void getConfig(SimpletypesConfig.Builder builder) {
+ }
+ }
+
+ private static class ComplexProducer implements ComplexInterface {
+ @Override
+ public void getConfig(ArraytypesConfig.Builder builder) {
+ }
+ @Override
+ public void getConfig(SimpletypesConfig.Builder builder) {
+ }
+ }
+
+ private static class RestartConfig extends ConfigInstance {
+ @SuppressWarnings("UnusedDeclaration")
+ private static boolean containsFieldsFlaggedWithRestart() {
+ return true;
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ private ChangesRequiringRestart getChangesRequiringRestart(RestartConfig newConfig) {
+ return new ChangesRequiringRestart("testing");
+ }
+ }
+
+ private static class NonRestartConfig extends ConfigInstance {}
+
+ @Test
+ public void requireThatConfigsProducedByInterfaceTakesParentIntoAccount() {
+ Set<ConfigKey<?>> configs = ReflectionUtil.configsProducedByInterface(ComplexProducer.class, "foo");
+ assertThat(configs.size(), is(2));
+ assertTrue(configs.contains(new ConfigKey<>(SimpletypesConfig.CONFIG_DEF_NAME, "foo", SimpletypesConfig.CONFIG_DEF_NAMESPACE)));
+ assertTrue(configs.contains(new ConfigKey<>(ArraytypesConfig.CONFIG_DEF_NAME, "foo", ArraytypesConfig.CONFIG_DEF_NAMESPACE)));
+ }
+
+ @Test
+ public void requireThatConfigsProducedByInterfaceAreFound() {
+ Set<ConfigKey<?>> configs = ReflectionUtil.configsProducedByInterface(SimpleProducer.class, "foo");
+ assertThat(configs.size(), is(1));
+ assertTrue(configs.contains(new ConfigKey<>(SimpletypesConfig.CONFIG_DEF_NAME, "foo", SimpletypesConfig.CONFIG_DEF_NAMESPACE)));
+ }
+
+ @Test
+ public void requireThatRestartMethodsAreDetectedProperly() {
+ assertFalse(ReflectionUtil.hasRestartMethods(NonRestartConfig.class));
+ assertTrue(ReflectionUtil.hasRestartMethods(RestartConfig.class));
+ }
+
+ @Test
+ public void requireThatRestartMethodsAreProperlyInvoked() {
+ assertTrue(ReflectionUtil.containsFieldsFlaggedWithRestart(RestartConfig.class));
+ assertEquals("testing", ReflectionUtil.getChangesRequiringRestart(new RestartConfig(), new RestartConfig()).getName());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void requireThatGetChangesRequiringRestartValidatesParameterTypes() {
+ ReflectionUtil.getChangesRequiringRestart(new RestartConfig(), new NonRestartConfig());
+ }
+
+
+}
diff --git a/config-model/src/test/java/helpers/CompareConfigTestHelper.java b/config-model/src/test/java/helpers/CompareConfigTestHelper.java
new file mode 100644
index 00000000000..f7709b4aebf
--- /dev/null
+++ b/config-model/src/test/java/helpers/CompareConfigTestHelper.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package helpers;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+import com.yahoo.io.IOUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Vegard Sjonfjell
+ */
+public class CompareConfigTestHelper {
+
+ public static void assertSerializedConfigFileEquals(String filename, String actual) throws IOException {
+ assertSerializedConfigEquals(IOUtils.readFile(new File(filename)), actual);
+ }
+
+ // Written this way to compare order independently but output error with order preserved
+ // Note that this means that if a test fails you'll also see spurious differences in the comparison
+ // from lines which are present in both but at different locations.
+ public static void assertSerializedConfigEquals(String expected, String actual) {
+ if ( ! sortLines(expected.trim()).equals(sortLines(actual.trim())))
+ assertEquals(expected, actual);
+ }
+
+ private static String sortLines(String fileData) {
+ final List<String> lines = Lists.newArrayList(Splitter.on('\n').split(fileData));
+ Collections.sort(lines);
+ return Joiner.on('\n').join(lines);
+ }
+
+}
diff --git a/config-model/src/test/processing/boldnonstring.sd b/config-model/src/test/processing/boldnonstring.sd
new file mode 100644
index 00000000000..a8b13b4e9ef
--- /dev/null
+++ b/config-model/src/test/processing/boldnonstring.sd
@@ -0,0 +1,14 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+search boldnonstring {
+ document boldnonstring {
+ field title type string {
+ indexing: summary | index
+ # index-to: title, default
+ }
+
+ field year4 type int {
+ indexing: summary | attribute
+ bolding: on
+ }
+ }
+}
diff --git a/config-model/src/test/resources/configdefinitions/anotherrestart.def b/config-model/src/test/resources/configdefinitions/anotherrestart.def
new file mode 100644
index 00000000000..44862adce79
--- /dev/null
+++ b/config-model/src/test/resources/configdefinitions/anotherrestart.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Test config for ConfigValueChangeValidatorTest
+namespace=test
+
+anothervalue int restart
diff --git a/config-model/src/test/resources/configdefinitions/arraytypes.def b/config-model/src/test/resources/configdefinitions/arraytypes.def
new file mode 100644
index 00000000000..3529b906c4a
--- /dev/null
+++ b/config-model/src/test/resources/configdefinitions/arraytypes.def
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Config containing only simple array types that can be used for testing
+# individual types in detail.
+namespace=test
+
+boolarr[] bool
+doublearr[] double
+enumarr[] enum { VAL1, VAL2 }
+intarr[] int
+longarr[] long
+stringarr[] string
diff --git a/config-model/src/test/resources/configdefinitions/function-test.def b/config-model/src/test/resources/configdefinitions/function-test.def
new file mode 100644
index 00000000000..5391ee1dc3c
--- /dev/null
+++ b/config-model/src/test/resources/configdefinitions/function-test.def
@@ -0,0 +1,73 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#
+# This def file should test most aspects of def files that makes a difference
+# for the autogenerated config classes. The goal is to trigger all blocks of
+# code in the code generators. This includes:
+#
+# - Use all legal special characters in the def file name, to ensure that those
+# that needs to be replaced in type names are actually replaced.
+# - Use the same enum type twice to verify that we dont declare or define it
+# twice.
+# - Use the same struct type twice for the same reason.
+# - Include arrays of primitives and structs.
+# - Include enum primitives and array of enums. Arrays of enums must be handled
+# specially by the C++ code.
+# - Include enums both with and without default values.
+# - Include primitive string, numbers & doubles both with and without default
+# values.
+# - Have an array within a struct, to verify that we correctly recurse.
+# - Reuse type name further within to ensure that this works.
+
+namespace=test
+
+# Some random bool without a default value. These comments exist to check
+ # that comment parsing works.
+bool_val bool
+ ## A bool with a default value set.
+bool_with_def bool default=false
+int_val int
+int_with_def int default=-545
+long_val long
+long_with_def long default=-50000000000
+double_val double
+double_with_def double default=-6.43
+# Another comment
+string_val string
+stringwithdef string default="foobar"
+enum_val enum { FOO, BAR, FOOBAR }
+enumwithdef enum { FOO2, BAR2, FOOBAR2 } default=BAR2
+onechoice enum { ONLYFOO } default=ONLYFOO
+refval reference
+refwithdef reference default=":parent:"
+fileVal file
+
+boolarr[] bool
+intarr[] int
+longarr[] long
+doublearr[] double
+stringarr[] string
+enumarr[] enum { ARRAY, VALUES }
+refarr[] reference
+fileArr[] file
+
+# A basic struct
+basicStruct.foo string default="basic"
+basicStruct.bar int
+basicStruct.intArr[] int
+
+# A struct of struct
+rootStruct.inner0.name string default="inner0"
+rootStruct.inner0.index int
+rootStruct.inner1.name string default="inner1"
+rootStruct.inner1.index int
+rootStruct.innerArr[].boolVal bool default=false
+rootStruct.innerArr[].stringVal string
+
+myarray[].intval int default=14
+myarray[].stringval[] string
+myarray[].enumval enum { INNER, ENUM, TYPE } default=TYPE
+myarray[].refval reference # Value in array without default
+myarray[].fileVal file
+myarray[].anotherarray[].foo int default=-4
+myarray[].myStruct.a int
+myarray[].myStruct.b int default=2
diff --git a/config-model/src/test/resources/configdefinitions/nonrestart.def b/config-model/src/test/resources/configdefinitions/nonrestart.def
new file mode 100644
index 00000000000..a5dd2e37ca0
--- /dev/null
+++ b/config-model/src/test/resources/configdefinitions/nonrestart.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Test config for ConfigValueChangeValidatorTest
+namespace=test
+
+plainvalue int
diff --git a/config-model/src/test/resources/configdefinitions/restart.def b/config-model/src/test/resources/configdefinitions/restart.def
new file mode 100644
index 00000000000..a7a7b55237e
--- /dev/null
+++ b/config-model/src/test/resources/configdefinitions/restart.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Test config for ConfigValueChangeValidatorTest
+namespace=test
+
+value int restart
diff --git a/config-model/src/test/resources/configdefinitions/simpletypes.def b/config-model/src/test/resources/configdefinitions/simpletypes.def
new file mode 100644
index 00000000000..314c67ae709
--- /dev/null
+++ b/config-model/src/test/resources/configdefinitions/simpletypes.def
@@ -0,0 +1,11 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Config containing only simple leaf types with default values, that can be used
+# for testing individual types in detail.
+namespace=test
+
+boolval bool default=false
+doubleval double default=0.0
+enumval enum { VAL1, VAL2 } default=VAL1
+intval int default=0
+longval long default=0
+stringval string default="s"
diff --git a/config-model/src/test/resources/configdefinitions/standard.def b/config-model/src/test/resources/configdefinitions/standard.def
new file mode 100644
index 00000000000..c44740b8f92
--- /dev/null
+++ b/config-model/src/test/resources/configdefinitions/standard.def
@@ -0,0 +1,10 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Config containing only simple leaf types with default values, that can be used
+# for testing individual types in detail.
+namespace=test
+
+basicStruct.intVal int default=0
+basicStruct.stringVal string default="s"
+stringArr[] string
+astring string default=""
+baseport int default=-1
diff --git a/config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/MultipleRestApisTest.scala b/config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/MultipleRestApisTest.scala
new file mode 100644
index 00000000000..f932e2c15c0
--- /dev/null
+++ b/config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/MultipleRestApisTest.scala
@@ -0,0 +1,137 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.jersey.xml
+
+import scala.language.implicitConversions
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilderTestBase._
+import com.yahoo.vespa.model.container.jersey.{JerseyHandler => ModelJerseyHandler, RestApiContext, RestApi}
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilderTestBase
+import MultipleRestApisTest._
+import org.junit.Test
+import scala.xml.Elem
+import org.w3c.dom.Element
+import com.yahoo.container.jdisc.JdiscBindingsConfig
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers._
+import com.yahoo.container.ComponentsConfig
+import com.yahoo.component.ComponentId
+import com.yahoo.container.di.config.JerseyBundlesConfig
+
+/**
+ * @author gjoranv
+ * @since 5.11
+ */
+
+class MultipleRestApisTest extends ContainerModelBuilderTestBase {
+
+ trait TestApp {
+ createModel(root, restApiXml)
+
+ val handler1 = getContainerComponentNested(ClusterId, HandlerId1).asInstanceOf[ModelJerseyHandler]
+ val handler2 = getContainerComponentNested(ClusterId, HandlerId2).asInstanceOf[ModelJerseyHandler]
+ val restApis = getContainerCluster(ClusterId).getRestApiMap
+ }
+
+ @Test
+ def cluster_has_all_rest_apis() {
+ new TestApp {
+ assertThat(restApis.size(), is(2))
+ }
+ }
+
+ @Test
+ def rest_apis_have_path_as_component_id() {
+ new TestApp {
+ assertTrue(restApis.get(ComponentId.fromString(Path1)).isInstanceOf[RestApi])
+ assertTrue(restApis.get(ComponentId.fromString(Path2)).isInstanceOf[RestApi])
+ }
+ }
+
+ @Test
+ def jersey_handler_has_correct_bindings() {
+ new TestApp {
+ assertThat(handler1, not(nullValue()))
+ assertThat(handler1.getServerBindings, hasItems(HttpBinding1, HttpsBinding1))
+
+ assertThat(handler2, not(nullValue()))
+ assertThat(handler2.getServerBindings, hasItems(HttpBinding2, HttpsBinding2))
+ }
+ }
+
+ @Test
+ def jersey_bindings_are_included_in_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[JdiscBindingsConfig], ClusterId)
+ assertThat(config.handlers(HandlerId1).serverBindings(), hasItems(HttpBinding1, HttpsBinding1))
+ assertThat(config.handlers(HandlerId2).serverBindings(), hasItems(HttpBinding2, HttpsBinding2))
+ }
+ }
+
+
+ @Test
+ def jersey_handler_for_each_rest_api_is_included_in_components_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[ComponentsConfig], ClusterId)
+ assertThat(config.toString, containsString(".id \"" + HandlerId1 + "\""))
+ assertThat(config.toString, containsString(".id \"" + HandlerId2 + "\""))
+ }
+ }
+
+ @Test
+ def jersey_bundles_component_for_each_rest_api_is_included_in_components_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[ComponentsConfig], ClusterId)
+ assertThat(config.toString, containsString(".id \"" + RestApiContextId1 + "\""))
+ assertThat(config.toString, containsString(".id \"" + RestApiContextId2 + "\""))
+ }
+ }
+
+ @Test
+ def each_rest_api_has_correct_bundle() {
+ new TestApp {
+ val restApiContext1 = restApis.get(ComponentId.fromString(Path1)).getContext
+ val restApiContext2 = restApis.get(ComponentId.fromString(Path2)).getContext
+
+ val bundlesConfig1 = root.getConfig(classOf[JerseyBundlesConfig], restApiContext1.getConfigId)
+ assertThat(bundlesConfig1.toString, containsString("bundle1"))
+ assertThat(bundlesConfig1.toString, not(containsString("bundle2")))
+
+ val bundlesConfig2 = root.getConfig(classOf[JerseyBundlesConfig], restApiContext2.getConfigId)
+ assertThat(bundlesConfig2.toString, containsString("bundle2"))
+ assertThat(bundlesConfig2.toString, not(containsString("bundle1")))
+ }
+ }
+
+}
+
+object MultipleRestApisTest {
+ val ClusterId = "container"
+
+ val Path1 = "rest_1"
+ val Path2 = "rest_2"
+ val HttpBinding1 = "http://*/" + Path1 + "/*"
+ val HttpsBinding1 = "https://*/" + Path1 + "/*"
+ val HttpBinding2 = "http://*/" + Path2 + "/*"
+ val HttpsBinding2 = "https://*/" + Path2 + "/*"
+
+ val HandlerId1 = ModelJerseyHandler.CLASS + "-" + Path1
+ val HandlerId2 = ModelJerseyHandler.CLASS + "-" + Path2
+ val RestApiContextId1 = RestApiContext.CONTAINER_CLASS + "-" + Path1
+ val RestApiContextId2 = RestApiContext.CONTAINER_CLASS + "-" + Path2
+
+ val restApiXml =
+ <container version="1.0" id={ClusterId}>
+ <rest-api path={Path1}>
+ <components bundle="bundle1" />
+ </rest-api>
+
+ <rest-api path={Path2}>
+ <components bundle="bundle2" />
+ </rest-api>
+ </container>
+
+ implicit def toDomElement(elem: Elem): Element = {
+ DomBuilderTest.parse(elem.toString())
+ }
+
+}
diff --git a/config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/RestApiTest.scala b/config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/RestApiTest.scala
new file mode 100644
index 00000000000..6eef04cb08f
--- /dev/null
+++ b/config-model/src/test/scala/com/yahoo/vespa/model/container/jersey/xml/RestApiTest.scala
@@ -0,0 +1,214 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.jersey.xml
+
+import scala.language.implicitConversions
+import RestApiTest._
+import scala.xml.Elem
+import scala.collection.JavaConverters._
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest
+import org.w3c.dom.Element
+import org.junit.{Ignore, Test}
+import org.junit.Assert._
+import com.yahoo.vespa.model.container.component.{Component, Handler}
+import com.yahoo.vespa.model.container.jersey.{JerseyHandler => ModelJerseyHandler, RestApi, Jersey2Servlet, RestApiContext}
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilderTestBase
+import com.yahoo.vespa.model.container.xml.ContainerModelBuilderTestBase._
+import com.yahoo.container.ComponentsConfig
+import org.hamcrest.CoreMatchers.{is, nullValue, notNullValue, not, containsString, hasItem, hasItems}
+import org.hamcrest.Matchers.{empty, contains}
+import com.yahoo.container.jdisc.JdiscBindingsConfig
+import com.yahoo.container.di.config.{JerseyInjectionConfig, JerseyBundlesConfig}
+import com.yahoo.container.config.jersey.JerseyInitConfig
+import com.yahoo.component.ComponentId
+
+/**
+ * @author gjoranv
+ * @since 5.5
+ */
+class RestApiTest extends ContainerModelBuilderTestBase {
+
+ trait TestApp {
+ createModel(root, restApiXml)
+ root.validate()
+ getContainerCluster(ClusterId).prepare()
+
+ val restApi = getContainerCluster(ClusterId).getRestApiMap.values().iterator().next()
+ val handler = getContainerComponentNested(ClusterId, HandlerId).asInstanceOf[ModelJerseyHandler]
+ val context = restApi.getContext
+ }
+
+ @Test
+ def jersey_handler_has_correct_bindings() {
+ new TestApp {
+ assertThat(handler, not(nullValue()))
+ assertThat(handler.getServerBindings, hasItems(HttpBinding, HttpsBinding))
+ }
+ }
+
+ @Test
+ def jersey_bindings_are_included_in_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[JdiscBindingsConfig], ClusterId)
+ assertThat(config.handlers(HandlerId).serverBindings(), hasItems(HttpBinding, HttpsBinding))
+ }
+ }
+
+ @Test
+ def jersey_handler_has_correct_bundle_spec() {
+ new TestApp {
+ assertThat(handler.model.bundleInstantiationSpec.bundle.stringValue(), is(ModelJerseyHandler.BUNDLE))
+ }
+ }
+
+ @Test
+ def config_has_correct_jersey_mapping() {
+ new TestApp {
+ val config = root.getConfig(classOf[JerseyInitConfig], handler.getConfigId)
+ assertThat(config.jerseyMapping, is(Path))
+ }
+ }
+
+ @Test
+ def resource_bundles_are_included_in_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[JerseyBundlesConfig], context.getConfigId)
+
+ assertThat(config.bundles.size, is(1))
+ assertThat(config.bundles(0).spec, is("my-jersey-bundle:1.0"))
+ }
+ }
+
+ @Test
+ def packages_to_scan_are_included_in_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[JerseyBundlesConfig], context.getConfigId)
+ assertThat(config.bundles(0).packages, contains("com.yahoo.foo"))
+ }
+ }
+
+ @Test
+ def jersey_handler_is_included_in_components_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[ComponentsConfig], ClusterId)
+ assertThat(config.toString, containsString(".id \"" + HandlerId + "\""))
+ }
+ }
+
+ @Test
+ def restApiContext_is_included_in_components_config() {
+ new TestApp {
+ val config = root.getConfig(classOf[ComponentsConfig], ClusterId)
+ assertThat(config.toString, containsString(".id \"" + RestApiContextId + "\""))
+ }
+ }
+
+ @Test
+ def all_non_restApi_components_are_injected_to_RestApiContext() {
+ new TestApp {
+ def restApiContextConfig(config: ComponentsConfig) =
+ (for {
+ component <- config.components.asScala
+ if component.classId == RestApiContext.CONTAINER_CLASS
+ } yield component).head
+
+ val componentsConfig = root.getConfig(classOf[ComponentsConfig], ClusterId)
+
+ val clusterChildrenComponentIds = getContainerCluster(ClusterId).getAllComponents.asScala.map(
+ child => child.getComponentId).toSet
+
+ val restApiChildrenComponentIds = restApi.getChildren.values.asScala.map(
+ child => child.asInstanceOf[Component[_, _]].getComponentId).toSet
+
+ //TODO: Review: replace with filtering against RestApiContext.isCycleGeneratingComponent
+ val cycleInducingComponents = Set("com.yahoo.container.handler.observability.ApplicationStatusHandler") map ComponentId.fromString
+
+ val expectedInjectedConfigIds = clusterChildrenComponentIds -- restApiChildrenComponentIds -- cycleInducingComponents
+
+ val injectedConfigIds = restApiContextConfig(componentsConfig).inject.asScala.map(
+ inject => ComponentId.fromString(inject.id()))
+
+ // Verify that the two sets are equal. Split in two asserts to get decent failure messages.
+ assertThat("Not all required components are injected",
+ (expectedInjectedConfigIds -- injectedConfigIds).asJavaCollection, empty[Any])
+ assertThat("We inject some components that should not be injected",
+ (injectedConfigIds -- expectedInjectedConfigIds).asJavaCollection, empty[Any])
+ }
+ }
+
+ @Ignore // TODO: use for naming components instead
+ @Test
+ def jdisc_components_can_be_injected() {
+ new TestApp {
+ val config = root.getConfig(classOf[JerseyInjectionConfig], context.getConfigId)
+ assertThat(config.inject(0).instance(), is("injectedHandler"))
+ assertThat(config.inject(0).forClass(), is("com.yahoo.handler.Handler"))
+ }
+ }
+
+ @Ignore // TODO: use for naming a non-existent component instead
+ @Test(expected = classOf[IllegalArgumentException])
+ def injecting_non_existent_component() {
+ val restApiXml =
+ <container version="1.0" id={ClusterId}>
+ <rest-api path={Path}>
+ <components bundle="my-jersey-bundle:1.0" />
+ <inject jdisc-component="non-existent" for-class="foo" />
+ </rest-api>
+
+ </container>
+
+ createModel(root, restApiXml)
+ root.validate()
+
+ }
+
+ @Test
+ def legacy_syntax_should_produce_valid_model() {
+ val legacyXml =
+ <container version="1.0" >
+ <handler id={ModelJerseyHandler.CLASS}>
+ <binding>{HttpBinding}</binding>
+ <config name="jdisc.jersey.jersey-handler">
+ <jerseyMapping>jersey</jerseyMapping>
+ </config>
+ </handler>
+ </container>
+
+ createModel(root, legacyXml)
+
+ val handler = getContainerComponent("container", ModelJerseyHandler.CLASS).asInstanceOf[Handler[_]]
+ assertThat(handler, not(nullValue()))
+ assertThat(handler.getServerBindings, hasItem(HttpBinding))
+
+ val bindingsConfig = root.getConfig(classOf[JdiscBindingsConfig], ClusterId)
+ assertThat(bindingsConfig.handlers(ModelJerseyHandler.CLASS).serverBindings(), hasItem(HttpBinding))
+ }
+
+}
+
+object RestApiTest {
+ val Path = "rest/api"
+ val HttpBinding = "http://*/" + Path + "/*"
+ val HttpsBinding = "https://*/" + Path + "/*"
+ val HandlerId = ModelJerseyHandler.CLASS + "-" + RestApi.idFromPath(Path)
+ val RestApiContextId = RestApiContext.CONTAINER_CLASS + "-" + RestApi.idFromPath(Path)
+ val InjectedComponentId = "injectedHandler"
+
+ val ClusterId = "container"
+
+ val restApiXml =
+ <container version="1.0" id={ClusterId} jetty="true">
+ <rest-api path={Path}>
+ <components bundle="my-jersey-bundle:1.0">
+ <package>com.yahoo.foo</package>
+ </components>
+ </rest-api>
+
+ <handler id={InjectedComponentId} />
+ </container>
+
+ implicit def toDomElement(elem: Elem): Element = {
+ DomBuilderTest.parse(elem.toString())
+ }
+
+}
diff --git a/config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala b/config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala
new file mode 100644
index 00000000000..6b9e80ee1a1
--- /dev/null
+++ b/config-model/src/test/scala/com/yahoo/vespa/model/container/search/ImplicitIndexingClusterTest.scala
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search
+
+import com.yahoo.config.model.provision.InMemoryProvisioner
+import com.yahoo.config.model.test.MockApplicationPackage
+import com.yahoo.vespa.model.test.utils.VespaModelCreatorWithMockPkg
+import org.junit.Test
+
+import org.junit.Assert.assertNotNull
+import scala.xml.{XML, Elem}
+import java.io.StringWriter
+import com.yahoo.config.model.deploy.{DeployProperties, DeployState}
+
+/**
+ * @author tonytv
+ */
+class ImplicitIndexingClusterTest {
+ @Test
+ def existing_jdisc_is_used_as_indexing_cluster_when_multitenant() {
+ val servicesXml =
+ <services version="1.0">
+ <jdisc version="1.0" id="jdisc">
+ <search />
+ <nodes count="1" />
+ </jdisc>
+
+ <content id="music" version="1.0">
+ <redundancy>1</redundancy>
+ <documents>
+ <document type="music" mode="index" />
+ </documents>
+ <nodes count="1" />
+ </content>
+ </services>
+
+
+ val vespaModel = buildMultiTenantVespaModel(servicesXml)
+ val jdisc = vespaModel.getContainerClusters.get("jdisc")
+ assertNotNull("Docproc not added to jdisc", jdisc.getDocproc)
+ assertNotNull("Indexing chain not added to jdisc", jdisc.getDocprocChains.allChains().getComponent("indexing"))
+ }
+
+
+ def buildMultiTenantVespaModel(servicesXml: Elem) = {
+ val properties = new DeployProperties.Builder().multitenant(true).hostedVespa(true).build()
+ val deployStateBuilder = new DeployState.Builder()
+ .properties(properties)
+ .modelHostProvisioner(new InMemoryProvisioner(true, "host1.yahoo.com", "host2.yahoo.com", "host3.yahoo.com"))
+
+ val writer = new StringWriter
+ XML.write(writer, servicesXml, "UTF-8", xmlDecl = true, doctype = null)
+ writer.close()
+
+ new VespaModelCreatorWithMockPkg(new MockApplicationPackage.Builder()
+ .withServices(writer.toString)
+ .withSearchDefinition(MockApplicationPackage.MUSIC_SEARCHDEFINITION)
+ .build())
+ .create(deployStateBuilder)
+ }
+}
diff --git a/config-model/src/test/scala/com/yahoo/vespa/model/container/search/searchchain/FederationSearcherTest.scala b/config-model/src/test/scala/com/yahoo/vespa/model/container/search/searchchain/FederationSearcherTest.scala
new file mode 100644
index 00000000000..781a05c79d5
--- /dev/null
+++ b/config-model/src/test/scala/com/yahoo/vespa/model/container/search/searchchain/FederationSearcherTest.scala
@@ -0,0 +1,186 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.search.searchchain
+
+import java.util.Optional
+
+import scala.language.implicitConversions
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.FunSuite
+import com.yahoo.search.searchchain.model.federation.{FederationOptions, FederationSearcherModel}
+import com.yahoo.component.{ComponentSpecification, ComponentId}
+import com.yahoo.component.chain.dependencies.Dependencies
+import java.util.Collections.{emptyList, emptySet}
+import com.yahoo.component.provider.ComponentRegistry
+
+import FederationSearcherTest._
+import com.yahoo.component.chain.model.ChainSpecification
+import com.yahoo.search.federation.FederationConfig
+import com.yahoo.config.ConfigInstance
+import com.yahoo.vespa.model.ConfigProducer
+import scala.reflect.ClassTag
+import scala.collection.JavaConversions._
+import scala.collection.breakOut
+import com.yahoo.vespa.model.container.search.searchchain.Source.GroupOption
+import com.yahoo.search.federation.sourceref.Target
+import com.yahoo.search.searchchain.model.federation.FederationSearcherModel.TargetSpec
+
+
+/**
+ * @author tonytv
+ */
+@RunWith(classOf[JUnitRunner])
+class FederationSearcherTest extends FunSuite{
+
+ class FederationFixture {
+ val federationSearchWithDefaultSources = newFederationSearcher(inheritDefaultSources = true)
+ val searchChainRegistry = new ComponentRegistry[SearchChain]
+ val sourceGroupRegistry = new SourceGroupRegistry
+
+ def initializeFederationSearcher(searcher: FederationSearcher = federationSearchWithDefaultSources) {
+ searcher.initialize(searchChainRegistry, sourceGroupRegistry)
+ }
+
+ def registerProviderWithSources(provider: Provider) = {
+ provider :: provider.getSources.toList foreach { chain => searchChainRegistry.register(chain.getId, chain) }
+ sourceGroupRegistry.addSources(provider)
+ }
+ }
+
+ class ProvidersWithSourceFixture extends FederationFixture {
+ val provider1 = createProvider("provider1")
+ val provider2 = createProvider("provider2")
+
+ provider1.addSource(createSource("source", GroupOption.leader))
+ provider2.addSource(createSource("source", GroupOption.participant))
+
+ registerProviderWithSources(provider1)
+ registerProviderWithSources(provider2)
+ initializeFederationSearcher()
+ }
+
+ test("default providers are inherited when inheritDefaultSources=true") {
+ val f = new FederationFixture
+ import f._
+
+ val providerId = "providerId"
+
+ registerProviderWithSources(createProvider(providerId))
+ initializeFederationSearcher()
+
+ val federationConfig = getConfig[FederationConfig](federationSearchWithDefaultSources)
+ val target = federationConfig.target(0)
+
+ assert( providerId === target.id() )
+ assert( target.searchChain(0).useByDefault(), "Not used by default" )
+ }
+
+ def toMapByKey[KEY, VALUE](collection: java.util.Collection[VALUE])(f: VALUE => KEY): Map[KEY, VALUE] =
+ collection.map(e => (f(e), e))(breakOut)
+
+ test("source groups are inherited when inheritDefaultSources=true") {
+ val f = new ProvidersWithSourceFixture
+ import f._
+
+ val federationConfig = getConfig[FederationConfig](federationSearchWithDefaultSources)
+ assert(federationConfig.target().size == 1)
+
+ val target = federationConfig.target(0)
+ assert(target.id() == "source")
+ assert(target.useByDefault(), "Not used by default")
+
+ //val chainsByProviderId = toMapByKey(target.searchChain())(_.providerId())
+
+ assert(Set("provider1", "provider2") === target.searchChain().map(_.providerId()).toSet)
+ }
+
+ test("source groups are not inherited when inheritDefaultSources=false") {
+ val f = new ProvidersWithSourceFixture
+ import f._
+
+ val federationSearcherWithoutDefaultSources = newFederationSearcher(inheritDefaultSources = false)
+ initializeFederationSearcher(federationSearcherWithoutDefaultSources)
+
+ val federationConfig = getConfig[FederationConfig](federationSearcherWithoutDefaultSources)
+ assert(federationConfig.target().size == 0)
+ }
+
+ test("leaders must be the first search chain in a target") {
+ val f = new ProvidersWithSourceFixture
+ import f._
+
+ val federationConfig = getConfig[FederationConfig](federationSearchWithDefaultSources)
+ val searchChain = federationConfig.target(0).searchChain
+
+ assert(searchChain.get(0).providerId() === "provider1")
+ assert(searchChain.get(1).providerId() === "provider2")
+
+ }
+
+ test("manually specified targets overrides inherited targets") {
+ val f = new FederationFixture
+ import f._
+
+ registerProviderWithSources(createProvider("provider1"))
+ val federation = newFederationSearcher(inheritDefaultSources = true,
+ targets = List(new TargetSpec("provider1", new FederationOptions().setTimeoutInMilliseconds(12345))))
+
+ initializeFederationSearcher(federation)
+
+ val federationConfig = getConfig[FederationConfig](federation)
+
+ assert(federationConfig.target().size === 1)
+ val target = federationConfig.target(0)
+
+ assert(target.searchChain().size === 1)
+ val searchChain = target.searchChain(0)
+
+ assert(searchChain.timeoutMillis() === 12345)
+ }
+
+
+ def newFederationSearcher(inheritDefaultSources: Boolean,
+ targets: java.util.List[TargetSpec] = emptyList()): FederationSearcher = {
+ new FederationSearcher(
+ new FederationSearcherModel("federation",
+ Dependencies.emptyDependencies(),
+ targets,
+ inheritDefaultSources),
+ Optional.empty())
+ }
+}
+
+object FederationSearcherTest {
+ implicit def toComponentId(name: String): ComponentId = ComponentId.fromString(name)
+ implicit def toComponentSpecification(name: String): ComponentSpecification = ComponentSpecification.fromString(name)
+
+ def newBuilder[T <: ConfigInstance.Builder](implicit c: ClassTag[T]): T = {
+ c.runtimeClass.newInstance().asInstanceOf[T]
+ }
+
+ def searchChainSpecification(id: ComponentId) =
+ new ChainSpecification(id, new ChainSpecification.Inheritance(null, null), emptyList(), emptySet())
+
+ def createProvider(id: ComponentId) =
+ new Provider(searchChainSpecification(id), new FederationOptions())
+
+ def createSource(id: ComponentId, groupOption: GroupOption) =
+ new Source(searchChainSpecification(id), new FederationOptions(), groupOption)
+
+
+ //TODO: TVT: move
+ def getConfig[T <: ConfigInstance : ClassTag](configProducer: ConfigProducer): T = {
+ val configClass = implicitly[ClassTag[T]].runtimeClass
+ val builderClass = configClass.getDeclaredClasses.collectFirst {case c if c.getSimpleName == "Builder" => c } getOrElse {
+ sys.error("No Builder class in ConfigInstance.")
+ }
+
+ val builder = builderClass.newInstance().asInstanceOf[AnyRef]
+ val getConfigMethod = configProducer.getClass.getMethod("getConfig", builderClass)
+
+ getConfigMethod.invoke(configProducer, builder)
+
+ configClass.getConstructor(builderClass).newInstance(builder).asInstanceOf[T]
+ }
+}
diff --git a/config-model/src/test/scala/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilderTest.scala b/config-model/src/test/scala/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilderTest.scala
new file mode 100644
index 00000000000..819fd052d82
--- /dev/null
+++ b/config-model/src/test/scala/com/yahoo/vespa/model/container/xml/BundleInstantiationSpecificationBuilderTest.scala
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml
+
+import com.yahoo.component.ComponentSpecification
+import com.yahoo.search.grouping.GroupingValidator
+
+import scala.language.implicitConversions
+import BundleInstantiationSpecificationBuilderTest._
+import com.yahoo.config.model.builder.xml.test.DomBuilderTest
+import org.hamcrest.CoreMatchers._
+import org.junit.Assert._
+import org.junit.Test
+import org.w3c.dom.Element
+
+import scala.xml.Elem
+
+/**
+ * @author gjoranv
+ * @since 5.45
+ */
+
+class BundleInstantiationSpecificationBuilderTest {
+
+ @Test
+ def bundle_is_not_replaced_for_user_defined_class() {
+ val userDefinedClass = "my own class that will also be set as bundle"
+ verifyExpectedBundle(userDefinedClass,
+ expectedBundle = userDefinedClass)
+ }
+
+ @Test
+ def bundle_is_replaced_for_internal_class() = {
+ val internalClass = classOf[GroupingValidator].getName
+ verifyExpectedBundle(internalClass,
+ expectedBundle = BundleMapper.searchAndDocprocBundle)
+ }
+
+ @Test
+ def bundle_is_not_replaced_for_internal_class_with_explicitly_set_bundle() = {
+ val internalClass = classOf[GroupingValidator].getName
+ val explicitBundle = "my-own-implementation"
+ verifyExpectedBundle(internalClass,
+ explicitBundle = Some(explicitBundle),
+ expectedBundle = explicitBundle)
+ }
+}
+
+object BundleInstantiationSpecificationBuilderTest {
+
+ def verifyExpectedBundle(className: String,
+ explicitBundle: Option[String] = None,
+ expectedBundle:String) = {
+ val xml = <component id="_" class={className} bundle={explicitBundle.orNull} />
+
+ val spec = BundleInstantiationSpecificationBuilder.build(xml, false)
+ assertThat(spec.bundle, is(ComponentSpecification.fromString(expectedBundle)))
+ }
+
+ implicit def toDomElement(elem: Elem): Element = {
+ DomBuilderTest.parse(elem.toString())
+ }
+}
diff --git a/config-model/src/test/scala/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilderTest.scala b/config-model/src/test/scala/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilderTest.scala
new file mode 100644
index 00000000000..4a437994656
--- /dev/null
+++ b/config-model/src/test/scala/com/yahoo/vespa/model/container/xml/ManhattanContainerModelBuilderTest.scala
@@ -0,0 +1,144 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.model.container.xml
+
+
+import org.junit.Test
+import scala.xml.{PrettyPrinter, Elem}
+
+import ManhattanContainerModelBuilderTest._
+import com.yahoo.config.model.test.MockRoot
+import org.apache.commons.io.IOUtils
+import com.yahoo.vespa.model.container.ContainerCluster
+import com.yahoo.vespa.model.container.component.{Component, AccessLogComponent}
+import scala.collection.JavaConversions._
+import scala.reflect.ClassTag
+import com.yahoo.config.model.producer.AbstractConfigProducer
+import com.yahoo.osgi.provider.model.ComponentModel
+import org.junit.Assert.{assertThat, assertNotNull}
+import org.hamcrest.CoreMatchers.is
+import com.yahoo.container.handler.VipStatusHandler
+import com.yahoo.config.model.builder.xml.XmlHelper.getDocumentBuilder
+import com.yahoo.vespa.model.container.search.searchchain.FederationSearcherTest
+import com.yahoo.container.jdisc.config.HttpServerConfig
+import com.yahoo.config.model.deploy.DeployState
+
+import scala.language.reflectiveCalls
+
+/**
+ * @author tonytv
+ */
+class ManhattanContainerModelBuilderTest {
+
+ val emptyJDiscElement = <jdisc version="1.0" />
+
+ @Test
+ def multiple_access_logs_configured() {
+ val container = buildManhattanContainer(
+ <jdisc version="1.0">
+ <accesslog type="yapache" fileNamePattern="myPattern" />
+ <accesslog type="vespa" fileNamePattern="myPattern" />
+ </jdisc>)
+ }
+
+ @Test
+ def status_html_and_akamai_handlers_configured() {
+ val container = buildManhattanContainer(emptyJDiscElement)
+
+ val vipStatusComponents = getComponentsWithModelClass[VipStatusHandler](container)
+ val ids = vipStatusComponents map { _.model.getComponentId.getName }
+
+ assertThat(ids.toSet, is(Set("status.html-status-handler", "akamai-status-handler")))
+ }
+
+ @Test
+ def http_server_added_automatically() {
+ val container = buildManhattanContainer(emptyJDiscElement)
+
+ assertThat(((container.getHttp.getHttpServer != null) && (container.getHttp.getHttpServer.getConnectorFactories.size() == 1)), is(true))
+ assertThat(container.getHttp.getHttpServer.getConnectorFactories.head.getListenPort, is(httpPort))
+ }
+
+ @Test
+ def only_the_first_http_server_is_kept() {
+ val container = buildManhattanContainer(
+ <jdisc version="1.0">
+ <http>
+ <server id="server1" port="123" />
+ <server id="server2" port="456" />
+ </http>
+ </jdisc>)
+
+ assertThat(((container.getHttp.getHttpServer != null) && (container.getHttp.getHttpServer.getConnectorFactories.size() == 1)), is(true))
+ assertThat(container.getHttp.getHttpServer.getComponentId.getName, is("jdisc-jetty"))
+ assertThat(container.getHttp.getHttpServer.getConnectorFactories.head.getName, is("server1"))
+ assertThat(container.getHttp.getHttpServer.getConnectorFactories.head.getListenPort, is(httpPort))
+ }
+
+ @Test
+ def filters_and_bindings_are_preserved() {
+ val container = buildManhattanContainer(
+ <jdisc version="1.0">
+ <http>
+ <filtering>
+ <filter id="my-filter" />
+ <request-chain id="my-chain">
+ <filter id="my-filter" />
+ <binding>http://*:123/my-binding</binding>
+ </request-chain>
+ </filtering>
+ <server id="server1" port="123" />
+ </http>
+ </jdisc>)
+
+ val binding = container.getHttp.getBindings.head
+ assertThat(binding.filterId.getName, is("my-chain"))
+ assertThat(binding.binding, is("http://*:123/my-binding"))
+
+ val filterChains = container.getHttp.getFilterChains
+ assertNotNull("Missing filter", filterChains.componentsRegistry().getComponent("my-filter"))
+ assertNotNull("Missing chain", filterChains.allChains().getComponent("my-chain"))
+ }
+}
+
+object ManhattanContainerModelBuilderTest {
+ type ACP = AbstractConfigProducer[_]
+ type COMPONENT = Component[_ <: ACP, _ <: ComponentModel]
+
+ val httpPort = 9876
+
+ def getComponents[T <: COMPONENT](cluster: ContainerCluster)(implicit tag: ClassTag[T]): Iterable[T] = {
+ fixType(cluster.getComponentsMap.values()) collect { case c: T => c }
+ }
+
+ def getComponentsWithModelClass[T <: AnyRef](cluster: ContainerCluster)(implicit tag: ClassTag[T]) = {
+ val className = tag.runtimeClass.getName
+ fixType(cluster.getAllComponents) filter { _.model.getClassId.getName == className }
+ }
+
+ def modelClassIdMatches(name: String): PartialFunction[COMPONENT, COMPONENT] = {
+ case c: COMPONENT if c.model.getClassId.getName == name => c
+ }
+
+ def fixType(components: java.util.Collection[_ <: Component[_, _]]): java.util.Collection[COMPONENT] =
+ components.asInstanceOf[java.util.Collection[COMPONENT]]
+
+ def buildManhattanContainer(elem: Elem) = {
+ val root = new MockRoot()
+ val containerModel = new ManhattanContainerModelBuilder(httpPort).build(DeployState.createTestState(), null, root, domElement(elem))
+ root.freezeModelTopology()
+ containerModel.getCluster()
+ }
+
+ def xmlStringBuilder(elem: Elem) = {
+ val printer = new PrettyPrinter(240, 2)
+ val builder = new StringBuilder
+ builder.append("<?xml version='1.0' encoding='utf-8' ?>\n")
+ printer.format(elem, builder)
+ builder
+ }
+
+ def domElement(elem: Elem) = {
+ val stream = IOUtils.toInputStream(xmlStringBuilder(elem))
+ getDocumentBuilder.parse(stream).getDocumentElement
+ }
+}
diff --git a/config-model/src/test/schema-test-files/hosts.xml b/config-model/src/test/schema-test-files/hosts.xml
new file mode 100755
index 00000000000..97b742177b0
--- /dev/null
+++ b/config-model/src/test/schema-test-files/hosts.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+
+ <host name="localhost">
+ <alias>host1</alias>
+ <alias>configserver</alias>
+ <alias>logserver</alias>
+ <alias>qrserver</alias>
+ <alias>distributor</alias>
+ <alias>feeder</alias>
+ <alias>rtx</alias>
+ <alias>tld</alias>
+ <alias>rtc-1</alias>
+ <alias>rtc-2</alias>
+ </host>
+
+ <host name="example.yahoo.com">
+ <alias>host2</alias>
+ <alias>distributor-3</alias>
+ </host>
+
+</hosts>
diff --git a/config-model/src/test/schema-test-files/major-version-services.xml b/config-model/src/test/schema-test-files/major-version-services.xml
new file mode 100644
index 00000000000..b1c57244176
--- /dev/null
+++ b/config-model/src/test/schema-test-files/major-version-services.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0" major-version="5">
+</services>
diff --git a/config-model/src/test/schema-test-files/services-hosted-explicit-admin.xml b/config-model/src/test/schema-test-files/services-hosted-explicit-admin.xml
new file mode 100644
index 00000000000..36e82021341
--- /dev/null
+++ b/config-model/src/test/schema-test-files/services-hosted-explicit-admin.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="4.0">
+ <slobroks><nodes count="3" flavor="small"/></slobroks>
+ <logservers><nodes count="1" dedicated="true"/></logservers>
+ </admin>
+
+ <jdisc id="container" version="1.0">
+ <nodes count="5" flavor="medium"/>
+ </jdisc>
+
+ <content id="search" version="1.0">
+ <redundancy>2</redundancy>
+ <controllers><nodes count="3" dedicated="true"/></controllers>
+ <nodes count="7" flavor="large" groups="12"/>
+ </content>
+
+</services>
diff --git a/config-model/src/test/schema-test-files/services-hosted.xml b/config-model/src/test/schema-test-files/services-hosted.xml
new file mode 100644
index 00000000000..1b3dad46462
--- /dev/null
+++ b/config-model/src/test/schema-test-files/services-hosted.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="3.0">
+ <nodes count="3" flavor="small"/>
+ </admin>
+
+ <jdisc id="container" version="1.0">
+ <nodes count="5" flavor="medium"/>
+ </jdisc>
+
+ <content id="search" version="1.0">
+ <redundancy>2</redundancy>
+ <nodes count="7" flavor="large" groups="12"/>
+ </content>
+
+</services>
diff --git a/config-model/src/test/schema-test-files/services.xml b/config-model/src/test/schema-test-files/services.xml
new file mode 100644
index 00000000000..7d75a406202
--- /dev/null
+++ b/config-model/src/test/schema-test-files/services.xml
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <config name="foo">
+ <intVal>1</intVal>
+ </config>
+
+ <admin version="2.0">
+ <adminserver hostalias="adminserver" />
+ <logserver hostalias="logserver" />
+ <slobroks>
+ <slobrok hostalias="configserver" />
+ <slobrok hostalias="qrserver" />
+ <slobrok hostalias="rtc-1" />
+ </slobroks>
+ </admin>
+
+ <config name="bar">
+ <intVal>1</intVal>
+ </config>
+
+ <jdisc id='qrsCluster_1' version='1.0'>
+ <rest-api path="jersey1">
+ <components bundle="my-bundle" />
+ <components bundle="other-bundle">
+ <package>com.yahoo.foo</package>
+ <package>com.yahoo.bar</package>
+ </components>
+<!--
+ <inject component="foo-component" for-name="com.yahoo.Foo" />
+ <inject component="bar-component" for-name="com.yahoo.Bar" />
+-->
+ </rest-api>
+ <rest-api path="jersey/2">
+ <components bundle="my-bundle" />
+ </rest-api>
+
+ <servlet id="my-servlet" class="com.yahoo.MyServlet" bundle="my-bundle">
+ <path>p/a/t/h</path>
+ </servlet>
+
+ <servlet id="my-servlet" class="com.yahoo.MyServlet" bundle="my-bundle">
+ <path>Apps/app_1.3-4/*</path>
+ <config name="foo">
+ <intVal>0</intVal>
+ </config>
+ <servlet-config>
+ <foo>bar</foo>
+ </servlet-config>
+ </servlet>
+
+ <http>
+ <filtering>
+ <filter id="com.yahoo.YcaFilter" bundle="mybundle">
+ <filter-config>
+ <foo>bar</foo>
+ <feature.enabled>true</feature.enabled>
+ </filter-config>
+ </filter>
+
+ <response-chain id="BaseChain">
+ <filter id="com.yahoo.Filter1" />
+ <filter id="com.yahoo.Filter2">
+ <filter-config />
+ </filter>
+ <binding>http://*/path1/*</binding>
+ <binding>http://*/path2/*</binding>
+ </response-chain>
+
+ <request-chain id="DerivedChain" inherits="BaseChain" excludes="com.yahoo.Filter1">
+ <filter id="com.yahoo.Filter3">
+ <config name="container.core.http.http-filter">
+ <param>
+ <item>
+ <name>yca.appid.allow</name>
+ <value>yahoo.vespa_factory.yca_test</value>
+ </item>
+ </param>
+ </config>
+ </filter>
+ <binding>http://*/path/*</binding>
+ </request-chain>
+ </filtering>
+
+ <server port="4080" id="myServer" />
+ <server port="4081" id="anotherServer">
+ <config name="container.jdisc.config.http-server">
+ <maxChunkSize>9999</maxChunkSize>
+ </config>
+ </server>
+ </http>
+
+ <document-api>
+ <binding>http://*/document-api/</binding>
+ <binding>https://*/document-api/</binding>
+ <abortondocumenterror>false</abortondocumenterror>
+ <retryenabled>false</retryenabled>
+ <timeout>5.55</timeout>
+ <route>default</route>
+ <maxpendingdocs>100</maxpendingdocs>
+ </document-api>
+
+ <search>
+ <searcher id='outer-searcher' />
+ <chain id='common'>
+ <searcher id='outer-searcher' />
+ <searcher id='inner-searcher' />
+ </chain>
+
+ <provider id='yca-provider' type='vespa' yca-application-id='my-app'>
+ <yca-proxy host='myhost' port='80'/>
+ <nodes>
+ <node host='sourcehost' port='12'/>
+ </nodes>
+ </provider>
+
+ <chain id="parentchain" searchers="one two">
+ <searcher id="three" />
+ <inherits>
+ <chain id="trope"/>
+ <chain id="kanoo"/>
+ <exclude id="notneededsearcher"/>
+ </inherits>
+ </chain>
+
+ <chain id="achain" searchers="asearcher anothersearcher" inherits="wonkaparentchain" excludes="notneededsearcher"/>
+ </search>
+
+ <processing>
+ <processor id='processor1' class='com.yahoo.test.Processor1' />
+ <chain id='default'>
+ <processor idref='processor1'/>
+ <processor id='processor2' class='com.yahoo.test.Processor2'/>
+ </chain>
+ </processing>
+
+ <handler id="bla" class="foo" bundle="bar" />
+ <handler id="bla2" class="foo$innerclass" bundle="bar" />
+ <handler id="bla2$innerclass" bundle="bar" />
+ <config name="foo">
+ <intVal>0</intVal>
+ <basicstruct>
+ <bsInt>1</bsInt>
+ <bsString>Hello</bsString>
+ </basicstruct>
+ </config>
+
+ <handler id="jdisc-handler">
+ <binding>http://*:*/HelloWorld</binding>
+ <binding>http://*:*/Status</binding>
+ <clientBinding>http://*:*/foo</clientBinding>
+ <clientBinding>http://*:*/bar</clientBinding>
+ </handler>
+
+ <client id="client-provider">
+ <binding>http://*:*/HelloWorld</binding>
+ <binding>http://*:*/Status</binding>
+ <serverBinding>http://*:*/foo</serverBinding>
+ <serverBinding>http://*:*/bar</serverBinding>
+ </client>
+
+ <server id="server-provider">
+
+ </server>
+
+ <nodes jvmargs="-XX:+PrintGCDetails -XX:+PrintGCTimeStamps">
+ <node hostalias="host1" />
+ <node hostalias="host1">
+ <server-port id="myServer" port="4090" />
+ </node>
+ </nodes>
+ </jdisc>
+
+</services>
diff --git a/config-model/src/test/schema-test-files/standalone-container.xml b/config-model/src/test/schema-test-files/standalone-container.xml
new file mode 100644
index 00000000000..7563af1b17f
--- /dev/null
+++ b/config-model/src/test/schema-test-files/standalone-container.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<jdisc id='qrsCluster_1' version='1.0'>
+ <rest-api path="jersey1">
+ <components bundle="my-bundle" />
+ <components bundle="other-bundle" />
+<!--
+ <inject component="foo-component" for-name="com.yahoo.Foo" />
+ <inject component="bar-component" for-name="com.yahoo.Bar" />
+-->
+ </rest-api>
+ <rest-api path="jersey/2">
+ <components bundle="my-bundle" />
+ <components bundle="other-bundle">
+ <package>com.yahoo.foo</package>
+ <package>com.yahoo.bar</package>
+ </components>
+ </rest-api>
+
+ <servlet id="my-servlet" class="com.yahoo.MyServlet" bundle="my-bundle">
+ <path>p/a/t/h</path>
+ </servlet>
+
+ <servlet id="my-servlet" class="com.yahoo.MyServlet" bundle="my-bundle">
+ <path>Apps/app_1.3-4/*</path>
+ <config name="foo">
+ <intVal>0</intVal>
+ </config>
+ <servlet-config>
+ <foo>bar</foo>
+ </servlet-config>
+ </servlet>
+
+ <http>
+ <filtering>
+ <filter id="com.yahoo.YcaFilter" bundle="mybundle">
+ <filter-config>
+ <foo>bar</foo>
+ <feature.enabled>true</feature.enabled>
+ </filter-config>
+ </filter>
+
+ <response-chain id="BaseChain">
+ <filter id="com.yahoo.Filter1" />
+ <filter id="com.yahoo.Filter2">
+ <filter-config />
+ </filter>
+ <binding>http://*/path1/*</binding>
+ <binding>http://*/path2/*</binding>
+ </response-chain>
+
+ <request-chain id="DerivedChain" inherits="BaseChain" excludes="com.yahoo.Filter1">
+ <filter id="com.yahoo.Filter3">
+ <config name="container.core.http.http-filter">
+ <param>
+ <item>
+ <name>yca.appid.allow</name>
+ <value>yahoo.vespa_factory.yca_test</value>
+ </item>
+ </param>
+ </config>
+ </filter>
+ <binding>http://*/path/*</binding>
+ </request-chain>
+ </filtering>
+
+ <server port="4080" id="myServer" />
+ <server port="4081" id="anotherServer" />
+ </http>
+
+ <document-api>
+ <binding>http://*/document-api/</binding>
+ <binding>https://*/document-api/</binding>
+ <abortondocumenterror>false</abortondocumenterror>
+ <retryenabled>false</retryenabled>
+ <timeout>5.55</timeout>
+ <route>default</route>
+ <maxpendingdocs>100</maxpendingdocs>
+ </document-api>
+
+ <search>
+ <searcher id='outer-searcher' />
+ <chain id='common'>
+ <searcher id='outer-searcher' />
+ <searcher id='inner-searcher' />
+ </chain>
+
+ <provider id='yca-provider' type='vespa' yca-application-id='my-app'>
+ <yca-proxy host='myhost' port='80'/>
+ <nodes>
+ <node host='sourcehost' port='12'/>
+ </nodes>
+ </provider>
+
+ <chain id="parentchain" searchers="one two">
+ <searcher id="three" />
+ <inherits>
+ <chain id="trope"/>
+ <chain id="kanoo"/>
+ <exclude id="notneededsearcher"/>
+ </inherits>
+ </chain>
+
+ <chain id="achain" searchers="asearcher anothersearcher" inherits="wonkaparentchain" excludes="notneededsearcher"/>
+ </search>
+
+ <processing>
+ <processor id='processor1' class='com.yahoo.test.Processor1' />
+ <chain id='default'>
+ <processor idref='processor1'/>
+ <processor id='processor2' class='com.yahoo.test.Processor2'/>
+ </chain>
+ </processing>
+
+ <handler id="bla" class="foo" bundle="bar" />
+ <config name="foo">
+ <intVal>0</intVal>
+ <basicstruct>
+ <bsInt>1</bsInt>
+ <bsString>Hello</bsString>
+ </basicstruct>
+ </config>
+
+ <handler id="jdisc-handler">
+ <binding>http://*:*/HelloWorld</binding>
+ <binding>http://*:*/Status</binding>
+ <clientBinding>http://*:*/foo</clientBinding>
+ <clientBinding>http://*:*/bar</clientBinding>
+ </handler>
+
+ <client id="client-provider">
+ <binding>http://*:*/HelloWorld</binding>
+ <binding>http://*:*/Status</binding>
+ <serverBinding>http://*:*/foo</serverBinding>
+ <serverBinding>http://*:*/bar</serverBinding>
+ </client>
+
+ <server id="server-provider" />
+
+
+</jdisc>
diff --git a/config-model/src/test/sh/test-schema.sh b/config-model/src/test/sh/test-schema.sh
new file mode 100755
index 00000000000..1be13452c1b
--- /dev/null
+++ b/config-model/src/test/sh/test-schema.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+pushd src/main && make
+popd
+
+jar="target/jing.jar"
+mainclass="com/thaiopensource/relaxng/util/Driver"
+
+cmd="java -jar $jar src/main/resources/schema/services.rng src/test/schema-test-files/services.xml"
+echo $cmd
+$cmd
+
+cmd="java -jar $jar src/main/resources/schema/services.rng src/test/schema-test-files/major-version-services.xml"
+echo $cmd
+$cmd
+
+cmd="java -jar $jar src/main/resources/schema/services.rng src/test/schema-test-files/standalone-container.xml"
+echo $cmd
+$cmd
+
+cmd="java -jar $jar src/main/resources/schema/services.rng src/test/schema-test-files/services-hosted.xml"
+echo $cmd
+$cmd
+
+cmd="java -jar $jar src/main/resources/schema/services.rng src/test/schema-test-files/services-hosted-explicit-admin.xml"
+echo $cmd
+$cmd