mirror of
https://github.com/etesync/android
synced 2025-01-11 08:10:58 +00:00
Synchronize exceptions of recurring events to the Calendar storage (server to client)
* Event class finds and processes exceptions of recurring events * workaround for iCloud and other services that provide RECURRENCE-ID as DATETIME even if the original event is an all-day event * VEvents are generated with all time zone definitions (including time zone definitions of exceptions)
This commit is contained in:
parent
f6eee6c910
commit
495cdf7c7e
@ -58,8 +58,10 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.SimpleTimeZone;
|
import java.util.SimpleTimeZone;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
@ -145,18 +147,43 @@ public class Event extends Resource {
|
|||||||
throw new InvalidResourceException(e);
|
throw new InvalidResourceException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// event
|
|
||||||
ComponentList events = ical.getComponents(Component.VEVENT);
|
ComponentList events = ical.getComponents(Component.VEVENT);
|
||||||
if (events == null || events.isEmpty())
|
if (events == null || events.isEmpty())
|
||||||
throw new InvalidResourceException("No VEVENT found");
|
throw new InvalidResourceException("No VEVENT found");
|
||||||
VEvent event = (VEvent)events.get(0);
|
|
||||||
|
|
||||||
|
// find master VEVENT (the one that is not an exception, i.e. the one without RECURRENCE-ID)
|
||||||
|
VEvent master = null;
|
||||||
|
for (Object objEvent : events) {
|
||||||
|
VEvent event = (VEvent)objEvent;
|
||||||
|
if (event.getRecurrenceId() == null) {
|
||||||
|
master = event;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (master == null)
|
||||||
|
throw new InvalidResourceException("No VEVENT without RECURRENCE-ID found");
|
||||||
|
// set event data from master VEVENT
|
||||||
|
fromVEvent(master);
|
||||||
|
|
||||||
|
// find and process exceptions
|
||||||
|
for (Object objEvent : events) {
|
||||||
|
VEvent event = (VEvent)objEvent;
|
||||||
|
if (event.getRecurrenceId() != null) {
|
||||||
|
Event exception = new Event(name, null);
|
||||||
|
exception.fromVEvent(event);
|
||||||
|
exceptions.add(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void fromVEvent(VEvent event) throws InvalidResourceException {
|
||||||
if (event.getUid() != null)
|
if (event.getUid() != null)
|
||||||
uid = event.getUid().getValue();
|
uid = event.getUid().getValue();
|
||||||
else {
|
else {
|
||||||
Log.w(TAG, "Received VEVENT without UID, generating new one");
|
Log.w(TAG, "Received VEVENT without UID, generating new one");
|
||||||
generateUID();
|
generateUID();
|
||||||
}
|
}
|
||||||
|
recurrenceId = event.getRecurrenceId();
|
||||||
|
|
||||||
if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null)
|
if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null)
|
||||||
throw new InvalidResourceException("Invalid start time/end time/duration");
|
throw new InvalidResourceException("Invalid start time/end time/duration");
|
||||||
@ -205,8 +232,10 @@ public class Event extends Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.alarms = event.getAlarms();
|
this.alarms = event.getAlarms();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public ByteArrayOutputStream toEntity() throws IOException {
|
public ByteArrayOutputStream toEntity() throws IOException {
|
||||||
@ -214,26 +243,38 @@ public class Event extends Resource {
|
|||||||
ical.getProperties().add(Version.VERSION_2_0);
|
ical.getProperties().add(Version.VERSION_2_0);
|
||||||
ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//EN"));
|
ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//EN"));
|
||||||
|
|
||||||
// "main event" (without exceptions)
|
// "master event" (without exceptions)
|
||||||
ComponentList components = ical.getComponents();
|
ComponentList components = ical.getComponents();
|
||||||
VEvent mainEvent = toVEvent(this);
|
VEvent master = toVEvent();
|
||||||
components.add(mainEvent);
|
components.add(master);
|
||||||
|
|
||||||
|
// remember used time zones
|
||||||
|
Set<net.fortuna.ical4j.model.TimeZone> usedTimeZones = new HashSet<>();
|
||||||
|
if (dtStart != null && dtStart.getTimeZone() != null)
|
||||||
|
usedTimeZones.add(dtStart.getTimeZone());
|
||||||
|
if (dtEnd != null && dtEnd.getTimeZone() != null)
|
||||||
|
usedTimeZones.add(dtEnd.getTimeZone());
|
||||||
|
|
||||||
// recurrence exceptions
|
// recurrence exceptions
|
||||||
for (Event exception : exceptions) {
|
for (Event exception : exceptions) {
|
||||||
VEvent vException = toVEvent(exception);
|
// create VEVENT for exception
|
||||||
vException.getProperties().add(mainEvent.getProperty(Property.UID));
|
VEvent vException = exception.toVEvent();
|
||||||
|
|
||||||
|
// set UID to UID of master event
|
||||||
|
vException.getProperties().add(master.getProperty(Property.UID));
|
||||||
|
|
||||||
components.add(vException);
|
components.add(vException);
|
||||||
|
|
||||||
|
// remember used time zones
|
||||||
|
if (exception.dtStart != null && exception.dtStart.getTimeZone() != null)
|
||||||
|
usedTimeZones.add(exception.dtStart.getTimeZone());
|
||||||
|
if (exception.dtEnd != null && exception.dtEnd.getTimeZone() != null)
|
||||||
|
usedTimeZones.add(exception.dtEnd.getTimeZone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// add VTIMEZONE components
|
// add VTIMEZONE components
|
||||||
net.fortuna.ical4j.model.TimeZone
|
for (net.fortuna.ical4j.model.TimeZone timeZone : usedTimeZones)
|
||||||
tzStart = (dtStart == null ? null : dtStart.getTimeZone()),
|
ical.getComponents().add(timeZone.getVTimeZone());
|
||||||
tzEnd = (dtEnd == null ? null : dtEnd.getTimeZone());
|
|
||||||
if (tzStart != null)
|
|
||||||
ical.getComponents().add(tzStart.getVTimeZone());
|
|
||||||
if (tzEnd != null && tzEnd != tzStart)
|
|
||||||
ical.getComponents().add(tzEnd.getVTimeZone());
|
|
||||||
|
|
||||||
CalendarOutputter output = new CalendarOutputter(false);
|
CalendarOutputter output = new CalendarOutputter(false);
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
@ -245,50 +286,50 @@ public class Event extends Resource {
|
|||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static VEvent toVEvent(Event e) {
|
protected VEvent toVEvent() {
|
||||||
VEvent event = new VEvent();
|
VEvent event = new VEvent();
|
||||||
PropertyList props = event.getProperties();
|
PropertyList props = event.getProperties();
|
||||||
|
|
||||||
if (e.uid != null)
|
if (uid != null)
|
||||||
props.add(new Uid(e.uid));
|
props.add(new Uid(uid));
|
||||||
if (e.recurrenceId != null)
|
if (recurrenceId != null)
|
||||||
props.add(e.recurrenceId);
|
props.add(recurrenceId);
|
||||||
|
|
||||||
props.add(e.dtStart);
|
props.add(dtStart);
|
||||||
if (e.dtEnd != null)
|
if (dtEnd != null)
|
||||||
props.add(e.dtEnd);
|
props.add(dtEnd);
|
||||||
if (e.duration != null)
|
if (duration != null)
|
||||||
props.add(e.duration);
|
props.add(duration);
|
||||||
|
|
||||||
if (e.rrule != null)
|
if (rrule != null)
|
||||||
props.add(e.rrule);
|
props.add(rrule);
|
||||||
if (e.rdate != null)
|
if (rdate != null)
|
||||||
props.add(e.rdate);
|
props.add(rdate);
|
||||||
if (e.exrule != null)
|
if (exrule != null)
|
||||||
props.add(e.exrule);
|
props.add(exrule);
|
||||||
if (e.exdate != null)
|
if (exdate != null)
|
||||||
props.add(e.exdate);
|
props.add(exdate);
|
||||||
|
|
||||||
if (e.summary != null && !e.summary.isEmpty())
|
if (summary != null && !summary.isEmpty())
|
||||||
props.add(new Summary(e.summary));
|
props.add(new Summary(summary));
|
||||||
if (e.location != null && !e.location.isEmpty())
|
if (location != null && !location.isEmpty())
|
||||||
props.add(new Location(e.location));
|
props.add(new Location(location));
|
||||||
if (e.description != null && !e.description.isEmpty())
|
if (description != null && !description.isEmpty())
|
||||||
props.add(new Description(e.description));
|
props.add(new Description(description));
|
||||||
|
|
||||||
if (e.status != null)
|
if (status != null)
|
||||||
props.add(e.status);
|
props.add(status);
|
||||||
if (!e.opaque)
|
if (!opaque)
|
||||||
props.add(Transp.TRANSPARENT);
|
props.add(Transp.TRANSPARENT);
|
||||||
|
|
||||||
if (e.organizer != null)
|
if (organizer != null)
|
||||||
props.add(e.organizer);
|
props.add(organizer);
|
||||||
props.addAll(e.attendees);
|
props.addAll(attendees);
|
||||||
|
|
||||||
if (e.forPublic != null)
|
if (forPublic != null)
|
||||||
event.getProperties().add(e.forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
|
event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE);
|
||||||
|
|
||||||
event.getAlarms().addAll(e.alarms);
|
event.getAlarms().addAll(alarms);
|
||||||
|
|
||||||
props.add(new LastModified());
|
props.add(new LastModified());
|
||||||
return event;
|
return event;
|
||||||
|
@ -35,6 +35,9 @@ import net.fortuna.ical4j.model.Dur;
|
|||||||
import net.fortuna.ical4j.model.Parameter;
|
import net.fortuna.ical4j.model.Parameter;
|
||||||
import net.fortuna.ical4j.model.ParameterList;
|
import net.fortuna.ical4j.model.ParameterList;
|
||||||
import net.fortuna.ical4j.model.PropertyList;
|
import net.fortuna.ical4j.model.PropertyList;
|
||||||
|
import net.fortuna.ical4j.model.TimeZone;
|
||||||
|
import net.fortuna.ical4j.model.TimeZoneRegistry;
|
||||||
|
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
|
||||||
import net.fortuna.ical4j.model.component.VAlarm;
|
import net.fortuna.ical4j.model.component.VAlarm;
|
||||||
import net.fortuna.ical4j.model.parameter.Cn;
|
import net.fortuna.ical4j.model.parameter.Cn;
|
||||||
import net.fortuna.ical4j.model.parameter.CuType;
|
import net.fortuna.ical4j.model.parameter.CuType;
|
||||||
@ -51,6 +54,7 @@ import net.fortuna.ical4j.model.property.RDate;
|
|||||||
import net.fortuna.ical4j.model.property.RRule;
|
import net.fortuna.ical4j.model.property.RRule;
|
||||||
import net.fortuna.ical4j.model.property.RecurrenceId;
|
import net.fortuna.ical4j.model.property.RecurrenceId;
|
||||||
import net.fortuna.ical4j.model.property.Status;
|
import net.fortuna.ical4j.model.property.Status;
|
||||||
|
import net.fortuna.ical4j.util.TimeZones;
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils;
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
|
||||||
@ -211,11 +215,12 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
// mark (recurring) events with changed/deleted exceptions as dirty
|
// mark (recurring) events with changed/deleted exceptions as dirty
|
||||||
String where = entryColumnID() + " IN (SELECT DISTINCT " + Events.ORIGINAL_ID + " FROM events WHERE " +
|
String where = entryColumnID() + " IN (SELECT DISTINCT " + Events.ORIGINAL_ID + " FROM events WHERE " +
|
||||||
Events.ORIGINAL_ID + " IS NOT NULL AND (" + Events.DIRTY + "=1 OR " + Events.DELETED + "=1))";
|
Events.ORIGINAL_ID + " IS NOT NULL AND (" + Events.DIRTY + "=1 OR " + Events.DELETED + "=1))";
|
||||||
Log.i(TAG, where);
|
|
||||||
ContentValues dirty = new ContentValues(1);
|
ContentValues dirty = new ContentValues(1);
|
||||||
dirty.put(CalendarContract.Events.DIRTY, 1);
|
dirty.put(CalendarContract.Events.DIRTY, 1);
|
||||||
try {
|
try {
|
||||||
providerClient.update(entriesURI(), dirty, where, null);
|
int rows = providerClient.update(entriesURI(), dirty, where, null);
|
||||||
|
if (rows > 0)
|
||||||
|
Log.d(TAG, rows + " event(s) marked as dirty because of dirty/deleted exceptions");
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.e(TAG, "Couldn't mark events with updated exceptions as dirty", e);
|
Log.e(TAG, "Couldn't mark events with updated exceptions as dirty", e);
|
||||||
}
|
}
|
||||||
@ -540,9 +545,6 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
|
|
||||||
builder = builder
|
builder = builder
|
||||||
.withValue(Events.CALENDAR_ID, id)
|
.withValue(Events.CALENDAR_ID, id)
|
||||||
.withValue(entryColumnRemoteName(), event.getName())
|
|
||||||
.withValue(entryColumnETag(), event.getETag())
|
|
||||||
.withValue(entryColumnUID(), event.getUid())
|
|
||||||
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
.withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0)
|
||||||
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
.withValue(Events.DTSTART, event.getDtStartInMillis())
|
||||||
.withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
|
.withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID())
|
||||||
@ -551,6 +553,25 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
.withValue(Events.GUESTS_CAN_MODIFY, 1)
|
.withValue(Events.GUESTS_CAN_MODIFY, 1)
|
||||||
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
|
.withValue(Events.GUESTS_CAN_SEE_GUESTS, 1);
|
||||||
|
|
||||||
|
RecurrenceId recurrenceId = event.getRecurrenceId();
|
||||||
|
if (recurrenceId == null) {
|
||||||
|
// this event is a "master event" (not an exception)
|
||||||
|
builder = builder
|
||||||
|
.withValue(entryColumnRemoteName(), event.getName())
|
||||||
|
.withValue(entryColumnETag(), event.getETag())
|
||||||
|
.withValue(entryColumnUID(), event.getUid());
|
||||||
|
} else {
|
||||||
|
// this event is an exception for a recurring event -> calculate
|
||||||
|
// 1. ORIGINAL_INSTANCE_TIME when the original instance would have occured (ms UTC)
|
||||||
|
// 2. ORIGINAL_ALL_DAY was the original instance an all-day event?
|
||||||
|
builder = builder.withValue(Events.ORIGINAL_SYNC_ID, event.getName());
|
||||||
|
|
||||||
|
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY is set in buildExceptions.
|
||||||
|
// It's not possible to use only the RECURRENCE-ID to calculate
|
||||||
|
// ORIGINAL_INSTANCE_TIME and ORIGINAL_ALL_DAY because iCloud sends DATE-TIME
|
||||||
|
// RECURRENCE-IDs even if the original event is an all-day event.
|
||||||
|
}
|
||||||
|
|
||||||
boolean recurring = false;
|
boolean recurring = false;
|
||||||
if (event.getRrule() != null) {
|
if (event.getRrule() != null) {
|
||||||
recurring = true;
|
recurring = true;
|
||||||
@ -612,8 +633,13 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
@Override
|
@Override
|
||||||
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
protected void addDataRows(Resource resource, long localID, int backrefIdx) {
|
||||||
Event event = (Event)resource;
|
Event event = (Event)resource;
|
||||||
|
// add exceptions
|
||||||
|
for (Event exception : event.getExceptions())
|
||||||
|
pendingOperations.add(buildException(newDataInsertBuilder(Events.CONTENT_URI, Events.ORIGINAL_ID, localID, backrefIdx), event, exception).build());
|
||||||
|
// add attendees
|
||||||
for (Attendee attendee : event.getAttendees())
|
for (Attendee attendee : event.getAttendees())
|
||||||
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build());
|
||||||
|
// add reminders
|
||||||
for (VAlarm alarm : event.getAlarms())
|
for (VAlarm alarm : event.getAlarms())
|
||||||
pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build());
|
pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build());
|
||||||
}
|
}
|
||||||
@ -621,15 +647,55 @@ public class LocalCalendar extends LocalCollection<Event> {
|
|||||||
@Override
|
@Override
|
||||||
protected void removeDataRows(Resource resource) {
|
protected void removeDataRows(Resource resource) {
|
||||||
Event event = (Event)resource;
|
Event event = (Event)resource;
|
||||||
|
// delete exceptions
|
||||||
|
pendingOperations.add(ContentProviderOperation.newDelete(entriesURI())
|
||||||
|
.withSelection(Events.ORIGINAL_ID + "=?",
|
||||||
|
new String[] { String.valueOf(event.getLocalID()) }).build());
|
||||||
|
// delete attendees
|
||||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
|
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI))
|
||||||
.withSelection(Attendees.EVENT_ID + "=?",
|
.withSelection(Attendees.EVENT_ID + "=?",
|
||||||
new String[] { String.valueOf(event.getLocalID()) }).build());
|
new String[] { String.valueOf(event.getLocalID()) }).build());
|
||||||
|
// delete reminders
|
||||||
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
|
pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI))
|
||||||
.withSelection(Reminders.EVENT_ID + "=?",
|
.withSelection(Reminders.EVENT_ID + "=?",
|
||||||
new String[] { String.valueOf(event.getLocalID()) }).build());
|
new String[] { String.valueOf(event.getLocalID()) }).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected Builder buildException(Builder builder, Event master, Event exception) {
|
||||||
|
buildEntry(builder, exception);
|
||||||
|
builder.withValue(Events.ORIGINAL_SYNC_ID, exception.getName());
|
||||||
|
|
||||||
|
// Some servers (iCloud, for instance) return RECURRENCE-ID with DATE-TIME even if
|
||||||
|
// the original event is an all-day event. Workaround: determine value of ORIGINAL_ALL_DAY
|
||||||
|
// by original event type (all-day or not) and not by whether RECURRENCE-ID is DATE or DATE-TIME.
|
||||||
|
|
||||||
|
RecurrenceId recurrenceId = exception.getRecurrenceId();
|
||||||
|
Date date = recurrenceId.getDate();
|
||||||
|
|
||||||
|
boolean originalAllDay = master.isAllDay();
|
||||||
|
long originalInstanceTime;
|
||||||
|
|
||||||
|
if (originalAllDay && date instanceof DateTime) {
|
||||||
|
String value = recurrenceId.getValue();
|
||||||
|
if (value.matches("^\\d{8}T\\d{6}$"))
|
||||||
|
try {
|
||||||
|
// no "Z" at the end indicates "local" time
|
||||||
|
// so this is a "local" time, but it should be a ical4j Date without time
|
||||||
|
date = new Date(value.substring(0, 8));
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.e(TAG, "Couldn't parse DATE part of DATE-TIME RECURRENCE-ID", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originalInstanceTime = date.getTime();
|
||||||
|
Log.i(TAG, "Original instance time: " + date.getTime()/1000);
|
||||||
|
|
||||||
|
builder.withValue(Events.ORIGINAL_INSTANCE_TIME, originalInstanceTime);
|
||||||
|
builder.withValue(Events.ORIGINAL_ALL_DAY, originalAllDay ? 1 : 0);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
protected Builder buildAttendee(Builder builder, Attendee attendee) {
|
||||||
Uri member = Uri.parse(attendee.getValue());
|
Uri member = Uri.parse(attendee.getValue());
|
||||||
|
@ -350,7 +350,7 @@ public abstract class LocalCollection<T extends Resource> {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) {
|
protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, int backrefIdx) {
|
||||||
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
|
Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri));
|
||||||
if (backrefIdx != -1)
|
if (backrefIdx != -1)
|
||||||
return builder.withValueBackReference(refFieldName, backrefIdx);
|
return builder.withValueBackReference(refFieldName, backrefIdx);
|
||||||
|
Loading…
Reference in New Issue
Block a user