@Test public void persist_only_last_field_mask() { final Iterable<String> iterableFields = singleton("TestEntity.firstField"); final String[] arrayFields = {"TestEntity.secondField"}; final Query query = factory().query() .select(TestEntity.class) .withMask(iterableFields) .withMask(arrayFields) .build(); assertNotNull(query); final FieldMask mask = query.getFieldMask(); final Collection<String> maskFields = mask.getPathsList(); assertSize(arrayFields.length, maskFields); assertThat(maskFields, contains(arrayFields)); }
EntityRecord findAndApplyFieldMask(I givenId, FieldMask fieldMask) { EntityRecord matchingResult = null; for (I recordId : filtered.keySet()) { if (recordId.equals(givenId)) { final Optional<EntityRecordWithColumns> record = get(recordId); if (!record.isPresent()) { continue; } EntityRecord.Builder matchingRecord = record.get() .getRecord() .toBuilder(); final Any state = matchingRecord.getState(); final TypeUrl typeUrl = TypeUrl.parse(state.getTypeUrl()); final Message wholeState = unpack(state); final Message maskedState = applyMask(fieldMask, wholeState, typeUrl); final Any processed = pack(maskedState); matchingRecord.setState(processed); matchingResult = matchingRecord.build(); } } return matchingResult; }
/** * Reads a record, which matches the specified {@linkplain RecordReadRequest request} * and applies a {@link FieldMask} to it. * * @param request the request to read the record * @param fieldMask fields to read. * @return the item with the given ID and with the {@code FieldMask} applied * or {@code Optional.absent()} if there is no record matching this request * @see #read(RecordReadRequest) */ public Optional<EntityRecord> read(RecordReadRequest<I> request, FieldMask fieldMask) { final Optional<EntityRecord> rawResult = read(request); if (!rawResult.isPresent()) { return Optional.absent(); } final EntityRecord.Builder builder = EntityRecord.newBuilder(rawResult.get()); final Any state = builder.getState(); final TypeUrl type = TypeUrl.parse(state.getTypeUrl()); final Message stateAsMessage = AnyPacker.unpack(state); final Message maskedState = FieldMasks.applyMask(fieldMask, stateAsMessage, type); final Any packedState = AnyPacker.pack(maskedState); builder.setState(packedState); return Optional.of(builder.build()); }
@Override public ImmutableCollection<Any> process(Query query) { final ImmutableList.Builder<Any> resultBuilder = ImmutableList.builder(); final Target target = query.getTarget(); final FieldMask fieldMask = query.getFieldMask(); final Iterator<? extends Entity> entities; if (target.getIncludeAll() && fieldMask.getPathsList() .isEmpty()) { entities = repository.loadAll(); } else { final EntityFilters filters = target.getFilters(); entities = repository.find(filters, fieldMask); } while (entities.hasNext()) { final Entity entity = entities.next(); final Message state = entity.getState(); final Any packedState = AnyPacker.pack(state); resultBuilder.add(packedState); } final ImmutableList<Any> result = resultBuilder.build(); return result; }
private Iterator<EntityRecord> doFetchWithFilters(Target target, FieldMask fieldMask) { final EntityFilters filters = target.getFilters(); final boolean idsAreDefined = !filters.getIdFilter() .getIdsList() .isEmpty(); if (!idsAreDefined) { return ImmutableList.<EntityRecord>of().iterator(); } final EntityIdFilter idFilter = filters.getIdFilter(); final Collection<AggregateStateId> stateIds = Collections2.transform(idFilter.getIdsList(), stateIdTransformer); final Iterator<EntityRecord> result = stateIds.size() == 1 ? readOne(stateIds.iterator() .next(), fieldMask) : readMany(stateIds, fieldMask); return result; }
private Iterator<EntityRecord> readOne(AggregateStateId singleId, FieldMask fieldMask) { final boolean shouldApplyFieldMask = !fieldMask.getPathsList() .isEmpty(); final RecordReadRequest<AggregateStateId> request = new RecordReadRequest<>(singleId); final Optional<EntityRecord> singleResult = shouldApplyFieldMask ? standStorage.read(request, fieldMask) : standStorage.read(request); Iterator<EntityRecord> result; if (!singleResult.isPresent()) { result = Collections.emptyIterator(); } else { result = Collections.singleton(singleResult.get()) .iterator(); } return result; }
@SuppressWarnings("MethodWithMultipleLoops") @Test public void read_all_messages_with_field_mask() { final List<ProjectId> ids = fillStorage(5); final String projectDescriptor = Project.getDescriptor() .getFullName(); @SuppressWarnings("DuplicateStringLiteralInspection") // clashes with non-related tests. final FieldMask fieldMask = maskForPaths(projectDescriptor + ".id", projectDescriptor + ".name"); final Iterator<EntityRecord> read = storage.readAll(fieldMask); final Collection<EntityRecord> readRecords = newArrayList(read); assertSize(ids.size(), readRecords); for (EntityRecord record : readRecords) { final Any packedState = record.getState(); final Project state = AnyPacker.unpack(packedState); assertMatchesMask(state, fieldMask); } }
@SuppressWarnings({"MethodWithMultipleLoops", "BreakStatement"}) @Test public void perform_bulk_read_with_field_mask_operation() { // Get a subset of IDs final List<ProjectId> ids = fillStorage(10).subList(0, 5); final String projectDescriptor = Project.getDescriptor() .getFullName(); final FieldMask fieldMask = maskForPaths(projectDescriptor + ".id", projectDescriptor + ".status"); final Iterator<EntityRecord> read = storage.readMultiple(ids, fieldMask); final Collection<EntityRecord> readRecords =newArrayList(read); assertSize(ids.size(), readRecords); // Check data consistency for (EntityRecord record : readRecords) { final Project state = checkProjectIdIsInList(record, ids); assertMatchesMask(state, fieldMask); } }
@Test public void exclude_non_active_records_from_entity_query() { final I archivedId = createId(42); final I deletedId = createId(314); final I activeId = createId(271); final E activeEntity = repository.create(activeId); final E archivedEntity = repository.create(archivedId); final E deletedEntity = repository.create(deletedId); delete((EventPlayingEntity) deletedEntity); archive((EventPlayingEntity) archivedEntity); // Fill the storage repository.store(activeEntity); repository.store(archivedEntity); repository.store(deletedEntity); final Iterator<E> found = repository.find(EntityFilters.getDefaultInstance(), FieldMask.getDefaultInstance()); final List<E> foundList = newArrayList(found); // Check results assertSize(1, foundList); final E actualEntity = foundList.get(0); assertEquals(activeEntity, actualEntity); }
@Test public void create_masks_for_given_field_tags() { final Descriptors.Descriptor descriptor = Project.getDescriptor(); final int[] fieldNumbers = {1, 2, 3}; @SuppressWarnings("DuplicateStringLiteralInspection") final String[] fieldNames = {"id", "name", "task"}; final FieldMask mask = FieldMasks.maskOf(descriptor, fieldNumbers); final List<String> paths = mask.getPathsList(); assertSize(fieldNumbers.length, paths); for (int i = 0; i < paths.size(); i++) { final String expectedPath = descriptor.getFullName() + '.' + fieldNames[i]; assertEquals(expectedPath, paths.get(i)); } }
@Test public void apply_only_non_empty_mask_to_single_item() { final FieldMask emptyMask = Given.fieldMask(); final Project origin = Given.newProject("read_whole_message"); final Project clone = Project.newBuilder(origin) .build(); final Project processed = FieldMasks.applyMask(emptyMask, origin, Given.TYPE); // Check object itself was returned assertTrue(processed == origin); // Check object was not changed assertTrue(processed.equals(clone)); }
@SuppressWarnings("ConstantConditions") // Converter nullability issues @Test public void read_single_record_with_mask() { final I id = newId(); final EntityRecord record = newStorageRecord(id); final RecordStorage<I> storage = getStorage(); storage.write(id, record); final Descriptors.Descriptor descriptor = newState(id).getDescriptorForType(); final FieldMask idMask = FieldMasks.maskOf(descriptor, 1); final RecordReadRequest<I> readRequest = new RecordReadRequest<>(id); final Optional<EntityRecord> optional = storage.read(readRequest, idMask); assertTrue(optional.isPresent()); final EntityRecord entityRecord = optional.get(); final Message unpacked = unpack(entityRecord.getState()); assertFalse(isDefault(unpacked)); }
@Test public void read_archived_records_if_specified() { final I activeRecordId = newId(); final I archivedRecordId = newId(); final EntityRecord activeRecord = newStorageRecord(activeRecordId, newState(activeRecordId)); final EntityRecord archivedRecord = newStorageRecord(archivedRecordId, newState(archivedRecordId)); final TestCounterEntity<I> activeEntity = new TestCounterEntity<>(activeRecordId); final TestCounterEntity<I> archivedEntity = new TestCounterEntity<>(archivedRecordId); archivedEntity.archive(); final RecordStorage<I> storage = getStorage(); storage.write(activeRecordId, create(activeRecord, activeEntity)); storage.write(archivedRecordId, create(archivedRecord, archivedEntity)); final EntityFilters filters = EntityFilters.newBuilder() .addFilter(all(eq(archived.toString(), true))) .build(); final EntityQuery<I> query = EntityQueries.from(filters, TestCounterEntity.class); final Iterator<EntityRecord> read = storage.readAll(query, FieldMask.getDefaultInstance()); assertSingleRecord(archivedRecord, read); }
@SuppressWarnings("ConstantConditions") private static void setupExpectedFindAllBehaviour( Map<ProjectId, Project> sampleProjects, StandTestProjectionRepository projectionRepository) { final Set<ProjectId> projectIds = sampleProjects.keySet(); final ImmutableCollection<Given.StandTestProjection> allResults = toProjectionCollection(projectIds); for (ProjectId projectId : projectIds) { when(projectionRepository.find(eq(projectId))) .thenReturn(Optional.of(new StandTestProjection(projectId))); } final Iterable<ProjectId> matchingIds = argThat(projectionIdsIterableMatcher(projectIds)); when(projectionRepository.loadAll(matchingIds, any(FieldMask.class))) .thenReturn(allResults.iterator()); when(projectionRepository.loadAll()) .thenReturn(allResults.iterator()); final EntityFilters matchingFilter = argThat(entityFilterMatcher(projectIds)); when(projectionRepository.find(matchingFilter, any(FieldMask.class))) .thenReturn(allResults.iterator()); }
/** * Converts a FieldMask to a string. */ public static String toString(FieldMask fieldMask) { // TODO(xiaofeng): Consider using com.google.common.base.Joiner here instead. StringBuilder result = new StringBuilder(); boolean first = true; for (String value : fieldMask.getPathsList()) { if (value.isEmpty()) { // Ignore empty paths. continue; } if (first) { first = false; } else { result.append(FIELD_PATH_SEPARATOR); } result.append(value); } return result.toString(); }
/** * Constructs a FieldMask for a list of field paths in a certain type. * * @throws IllegalArgumentException if any of the field path is not valid. */ // TODO(xiaofeng): Consider renaming fromStrings() public static FieldMask fromStringList(Class<? extends Message> type, Iterable<String> paths) { FieldMask.Builder builder = FieldMask.newBuilder(); for (String path : paths) { if (path.isEmpty()) { // Ignore empty field paths. continue; } if (type != null && !isValid(type, path)) { throw new IllegalArgumentException(path + " is not a valid path for " + type); } builder.addPaths(path); } return builder.build(); }
public void testToString() throws Exception { assertEquals("", FieldMaskUtil.toString(FieldMask.getDefaultInstance())); FieldMask mask = FieldMask.newBuilder().addPaths("foo").build(); assertEquals("foo", FieldMaskUtil.toString(mask)); mask = FieldMask.newBuilder().addPaths("foo").addPaths("bar").build(); assertEquals("foo,bar", FieldMaskUtil.toString(mask)); // Empty field paths are ignored. mask = FieldMask.newBuilder() .addPaths("") .addPaths("foo") .addPaths("") .addPaths("bar") .addPaths("") .build(); assertEquals("foo,bar", FieldMaskUtil.toString(mask)); }
public void testFromFieldNumbers() throws Exception { FieldMask mask = FieldMaskUtil.fromFieldNumbers(TestAllTypes.class); assertEquals(0, mask.getPathsCount()); mask = FieldMaskUtil.fromFieldNumbers( TestAllTypes.class, TestAllTypes.OPTIONAL_INT32_FIELD_NUMBER); assertEquals(1, mask.getPathsCount()); assertEquals("optional_int32", mask.getPaths(0)); mask = FieldMaskUtil.fromFieldNumbers( TestAllTypes.class, TestAllTypes.OPTIONAL_INT32_FIELD_NUMBER, TestAllTypes.OPTIONAL_INT64_FIELD_NUMBER); assertEquals(2, mask.getPathsCount()); assertEquals("optional_int32", mask.getPaths(0)); assertEquals("optional_int64", mask.getPaths(1)); try { int invalidFieldNumber = 1000; mask = FieldMaskUtil.fromFieldNumbers(TestAllTypes.class, invalidFieldNumber); fail("Exception is expected."); } catch (IllegalArgumentException expected) { } }
/** * Generates a new instance of {@link Query} regarding all the set parameters. * * @return the built {@link Query} */ public Query build() { final FieldMask mask = composeMask(); // Implying AnyPacker.pack to be idempotent final Set<Any> entityIds = composeIdPredicate(); final Query result = queryFactory.composeQuery(targetType, entityIds, columns, mask); return result; }
@Nullable private FieldMask composeMask() { if (fieldMask == null || fieldMask.isEmpty()) { return null; } final FieldMask mask = FieldMask.newBuilder() .addAllPaths(fieldMask) .build(); return mask; }
static Query.Builder queryBuilderFor(Class<? extends Message> entityClass, @Nullable Set<? extends Message> ids, @Nullable Set<CompositeColumnFilter> columnFilters, @Nullable FieldMask fieldMask) { checkNotNull(entityClass); final Target target = composeTarget(entityClass, ids, columnFilters); final Query.Builder queryBuilder = Query.newBuilder() .setTarget(target); if (fieldMask != null) { queryBuilder.setFieldMask(fieldMask); } return queryBuilder; }
Query composeQuery(Class<? extends Message> entityClass, @Nullable Set<? extends Message> ids, @Nullable Set<CompositeColumnFilter> columnFilters, @Nullable FieldMask fieldMask) { checkNotNull(entityClass, "The class of Entity must be specified for a Query"); final Query.Builder builder = queryBuilderFor(entityClass, ids, columnFilters, fieldMask); builder.setId(newQueryId()) .setContext(actorContext); return builder.build(); }
private void verifyTargetAndContext(Topic topic) { assertNotNull(topic); assertNotNull(topic.getId()); assertEquals(TARGET_ENTITY_TYPE_NAME.value(), topic.getTarget() .getType()); assertEquals(FieldMask.getDefaultInstance(), topic.getFieldMask()); final ActorContext actualContext = topic.getContext(); verifyContext(actualContext); }
@Test public void create_queries_with_field_mask() { final String fieldName = "TestEntity.firstField"; final Query query = factory().query() .select(TestEntity.class) .withMask(fieldName) .build(); assertNotNull(query); assertTrue(query.hasFieldMask()); final FieldMask mask = query.getFieldMask(); final Collection<String> fieldNames = mask.getPathsList(); assertSize(1, fieldNames); assertContains(fieldName, fieldNames); }
private static void verifyMultiplePathsInQuery(String[] paths, Query readAllWithPathFilteringQuery) { final FieldMask fieldMask = readAllWithPathFilteringQuery.getFieldMask(); assertEquals(paths.length, fieldMask.getPathsCount()); final ProtocolStringList pathsList = fieldMask.getPathsList(); for (String expectedPath : paths) { assertTrue(pathsList.contains(expectedPath)); } }
private static void verifySinglePathInQuery(String expectedEntityPath, Query query) { final FieldMask fieldMask = query.getFieldMask(); assertEquals(1, fieldMask.getPathsCount()); // as we set the only path value. final String firstPath = fieldMask.getPaths(0); assertEquals(expectedEntityPath, firstPath); }
static <I, E extends AbstractEntity<I, S>, S extends Message> EntityStorageConverter<I, E, S> forAllFields(TypeUrl entityStateType, EntityFactory<I, E> factory) { return new DefaultEntityStorageConverter<>(entityStateType, factory, FieldMask.getDefaultInstance()); }
protected EntityStorageConverter(TypeUrl entityStateType, EntityFactory<I, E> factory, FieldMask fieldMask) { super(); this.fieldMask = checkNotNull(fieldMask); this.entityFactory = factory; this.entityStateType = entityStateType; }
/** * Obtains iterator over {@link EntityRecord} for entities matching the passed filters. * * @param filters the filters for filtering entities * @param fieldMask the mask to apply for returned records * @return an iterator over the matching records */ @Internal public Iterator<EntityRecord> findRecords(EntityFilters filters, FieldMask fieldMask) { checkNotNull(filters); checkNotNull(fieldMask); final EntityQuery<I> entityQuery = EntityQueries.from(filters, getEntityClass()); final EntityQuery<I> completeQuery = toCompleteQuery(entityQuery); return recordStorage().readAll(completeQuery, fieldMask); }
/** * Creates a new instance of {@code FieldMask} basing on the target type * {@link Descriptor descriptor} and field tags defined in the Protobuf message. * * @param typeDescriptor {@link Descriptor descriptor} of the type to create a mask for. * @param fieldTags field tags to include into the mask. * @return an instance of {@code FieldMask} for the target type with the fields specified. */ public static FieldMask maskOf(Descriptor typeDescriptor, int... fieldTags) { if (fieldTags.length == 0) { return FieldMask.getDefaultInstance(); } final FieldMask.Builder result = FieldMask.newBuilder(); for (int fieldNumber : fieldTags) { final Descriptors.FieldDescriptor field = typeDescriptor.findFieldByNumber(fieldNumber); final String fieldPath = field.getFullName(); result.addPaths(fieldPath); } return result.build(); }
/** * Applies the given {@code FieldMask} to given collection of {@link Message}s. * Does not change the {@link Collection} itself. * * <p>In case the {@code FieldMask} instance contains invalid field declarations, they are * ignored and do not affect the execution result. * * @param mask {@code FieldMask} to apply to each item of the input {@link Collection}. * @param messages {@link Message}s to filter. * @param type type of the {@link Message}s. * @return messages with the {@code FieldMask} applied */ @Nonnull public static <M extends Message, B extends Message.Builder> Collection<M> applyMask(FieldMask mask, Collection<M> messages, TypeUrl type) { checkNotNull(mask); checkNotNull(messages); checkNotNull(type); final List<M> filtered = new LinkedList<>(); final ProtocolStringList filter = mask.getPathsList(); final Class<B> builderClass = getBuilderForType(type); if (filter.isEmpty() || builderClass == null) { return Collections.unmodifiableCollection(messages); } try { final Constructor<B> builderConstructor = builderClass.getDeclaredConstructor(); builderConstructor.setAccessible(true); for (Message wholeMessage : messages) { final M message = messageForFilter(filter, builderConstructor, wholeMessage); filtered.add(message); } } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { // If any reflection failure happens, return all the data without any mask applied. log().warn(format(CONSTRUCTOR_INVOCATION_ERROR_LOGGING_PATTERN, builderClass.getCanonicalName()), e); return Collections.unmodifiableCollection(messages); } return Collections.unmodifiableList(filtered); }
private static <M extends Message, B extends Message.Builder> M doApply(FieldMask mask, M message, TypeUrl type) { checkNotNull(mask); checkNotNull(message); checkNotNull(type); final ProtocolStringList filter = mask.getPathsList(); final Class<B> builderClass = getBuilderForType(type); if (builderClass == null) { return message; } try { final Constructor<B> builderConstructor = builderClass.getDeclaredConstructor(); builderConstructor.setAccessible(true); final M result = messageForFilter(filter, builderConstructor, message); return result; } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { log().warn(format(CONSTRUCTOR_INVOCATION_ERROR_LOGGING_PATTERN, builderClass.getCanonicalName()), e); return message; } }
Iterator<Event> iterator(EventStreamQuery query) { checkNotNull(query); final EntityFilters filters = toEntityFilters(query); final Iterator<EEntity> entities = find(filters, FieldMask.getDefaultInstance()); // A predicate on the Event message and EventContext fields. final Predicate<EEntity> detailedLookupFilter = createEntityFilter(query); final Iterator<EEntity> filtered = filter(entities, detailedLookupFilter); final List<EEntity> entityList = newArrayList(filtered); sort(entityList, comparator()); final Iterator<Event> result = transform(entityList.iterator(), getEvent()); return result; }
Map<I, EntityRecord> readAllRecords(FieldMask fieldMask) { if (fieldMask.getPathsList() .isEmpty()) { return readAllRecords(); } if (isEmpty()) { return ImmutableMap.of(); } final ImmutableMap.Builder<I, EntityRecord> result = ImmutableMap.builder(); for (Map.Entry<I, EntityRecordWithColumns> storageEntry : filtered.entrySet()) { final I id = storageEntry.getKey(); final EntityRecord rawRecord = storageEntry.getValue() .getRecord(); final TypeUrl type = TypeUrl.parse(rawRecord.getState() .getTypeUrl()); final Any recordState = rawRecord.getState(); final Message stateAsMessage = unpack(recordState); final Message processedState = applyMask(fieldMask, stateAsMessage, type); final Any packedState = pack(processedState); final EntityRecord resultingRecord = EntityRecord.newBuilder() .setState(packedState) .build(); result.put(id, resultingRecord); } return result.build(); }
@Override public Iterator<EntityRecord> readAllByType(final TypeUrl type, FieldMask fieldMask) { final Iterator<EntityRecord> allRecords = readAll(fieldMask); final Iterator<EntityRecord> result = filterByType(allRecords, type); return result; }
@Override protected Iterator<EntityRecord> readMultipleRecords(final Iterable<I> givenIds, FieldMask fieldMask) { final TenantRecords<I> storage = getStorage(); // It is not possible to return an immutable collection, // since null may be present in it. final Collection<EntityRecord> result = new LinkedList<>(); for (I givenId : givenIds) { final EntityRecord matchingResult = storage.findAndApplyFieldMask(givenId, fieldMask); result.add(matchingResult); } return result.iterator(); }
@Override public ImmutableCollection<Any> process(Query query) { final ImmutableList.Builder<Any> resultBuilder = ImmutableList.builder(); Iterator<EntityRecord> stateRecords; final Target target = query.getTarget(); final FieldMask fieldMask = query.getFieldMask(); final boolean shouldApplyFieldMask = !fieldMask.getPathsList() .isEmpty(); if (target.getIncludeAll()) { stateRecords = shouldApplyFieldMask ? standStorage.readAllByType(type, fieldMask) : standStorage.readAllByType(type); } else { stateRecords = doFetchWithFilters(target, fieldMask); } while (stateRecords.hasNext()) { final EntityRecord record = stateRecords.next(); final Any state = record.getState(); resultBuilder.add(state); } final ImmutableList<Any> result = resultBuilder.build(); return result; }
private Iterator<EntityRecord> readMany(Collection<AggregateStateId> stateIds, FieldMask fieldMask) { final boolean applyFieldMask = !fieldMask.getPathsList() .isEmpty(); final Iterator<EntityRecord> bulkReadResults = applyFieldMask ? standStorage.readMultiple(stateIds, fieldMask) : standStorage.readMultiple(stateIds); final Iterator<EntityRecord> result = Iterators.filter(bulkReadResults, notNull()); return result; }
@Test public void create_instance_with_FieldMask() throws Exception { final FieldMask fieldMask = FieldMask.newBuilder() .addPaths("foo.bar") .build(); final EntityStorageConverter<Long, TestEntity, StringValue> withMasks = converter.withFieldMask(fieldMask); assertEquals(fieldMask, withMasks.getFieldMask()); }
@Test public void find_entities_by_query() { final I id1 = createId(271); final I id2 = createId(314); final Class<E> entityClass = repository.getEntityClass(); final E entity1 = Given.entityOfClass(entityClass) .withId(id1) .build(); final E entity2 = Given.entityOfClass(entityClass) .withId(id2) .build(); repository.store(entity1); repository.store(entity2); final String fieldPath = "idString"; final StringValue fieldValue = StringValue.newBuilder() .setValue(id1.toString()) .build(); final ColumnFilter filter = eq(fieldPath, fieldValue); final CompositeColumnFilter aggregatingFilter = CompositeColumnFilter.newBuilder() .addFilter(filter) .setOperator(ALL) .build(); final EntityFilters filters = EntityFilters.newBuilder() .addFilter(aggregatingFilter) .build(); final Collection<E> found = newArrayList(repository.find(filters, FieldMask.getDefaultInstance())); assertSize(1, found); assertContains(entity1, found); assertNotContains(entity2, found); }