<commit_before>/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/
 */
package org.phenotips.studies.family.internal;

import org.phenotips.Constants;
import org.phenotips.data.Patient;
import org.phenotips.data.PatientRepository;
import org.phenotips.data.permissions.Owner;
import org.phenotips.security.authorization.AuthorizationService;
import org.phenotips.studies.family.Family;
import org.phenotips.studies.family.FamilyRepository;
import org.phenotips.studies.family.Pedigree;
import org.phenotips.studies.family.PedigreeProcessor;
import org.phenotips.studies.family.exceptions.PTException;
import org.phenotips.studies.family.exceptions.PTInternalErrorException;
import org.phenotips.studies.family.exceptions.PTInvalidFamilyIdException;
import org.phenotips.studies.family.exceptions.PTInvalidPatientIdException;
import org.phenotips.studies.family.exceptions.PTNotEnoughPermissionsOnFamilyException;
import org.phenotips.studies.family.exceptions.PTNotEnoughPermissionsOnPatientException;
import org.phenotips.studies.family.exceptions.PTPatientAlreadyInAnotherFamilyException;
import org.phenotips.studies.family.exceptions.PTPatientNotInFamilyException;
import org.phenotips.studies.family.exceptions.PTPedigreeContainesSamePatientMultipleTimesException;

import org.xwiki.component.annotation.Component;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.query.Query;
import org.xwiki.query.QueryException;
import org.xwiki.query.QueryManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.users.User;

import java.util.LinkedList;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.slf4j.Logger;

import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;

/**
 * Provides utility methods for working with family documents and patients.
 *
 * @version $Id$
 * @since 1.4
 */
@Component(roles = FamilyRepository.class)
@Singleton
public class PhenotipsFamilyRepository implements FamilyRepository
{
    private static final String PREFIX = "FAM";

    private static final EntityReference FAMILY_TEMPLATE =
        new EntityReference("FamilyTemplate", EntityType.DOCUMENT, Constants.CODE_SPACE_REFERENCE);

    private static final String FAMILY_REFERENCE_FIELD = "reference";

    @Inject
    private Logger logger;

    @Inject
    private Provider<XWikiContext> provider;

    @Inject
    private PatientRepository patientRepository;

    @Inject
    private PhenotipsFamilyPermissions familyPermissions;

    @Inject
    private AuthorizationService authorizationService;

    @Inject
    private PedigreeProcessor pedigreeConverter;

    /** Runs queries for finding families. */
    @Inject
    private QueryManager qm;

    @Inject
    @Named("current")
    private DocumentReferenceResolver<String> referenceResolver;

    /** Fills in missing reference fields with those from the current context document to create a full reference. */
    @Inject
    @Named("current")
    private DocumentReferenceResolver<EntityReference> entityReferenceResolver;

    @Override
    public Family createFamily(User creator)
    {
        XWikiDocument newFamilyDocument = null;
        try {
            newFamilyDocument = this.createFamilyDocument(creator);
        } catch (Exception e) {
            this.logger.error("Could not create a new family document: {}", e.getMessage());
        }
        if (newFamilyDocument == null) {
            this.logger.debug("Could not create a family document");
            return null;
        }
        return new PhenotipsFamily(newFamilyDocument);
    }

    @Override
    public synchronized boolean deleteFamily(Family family, User updatingUser, boolean deleteAllMembers)
    {
        if (!canDeleteFamily(family, updatingUser, deleteAllMembers, false)) {
            return false;
        }
        if (deleteAllMembers) {
            for (Patient patient : family.getMembers()) {
                if (!this.patientRepository.delete(patient)) {
                    this.logger.error("Failed to delete patient [{}] - deletion of family [{}] aborted",
                        patient.getId(), family.getId());
                    return false;
                }
            }
        } else if (!this.forceRemoveAllMembers(family, updatingUser)) {
            return false;
        }

        try {
            XWikiContext context = this.provider.get();
            XWiki xwiki = context.getWiki();
            xwiki.deleteDocument(xwiki.getDocument(family.getXDocument(), context), context);
        } catch (XWikiException ex) {
            this.logger.error("Failed to delete family document [{}]: {}", family.getId(), ex.getMessage());
            return false;
        }
        return true;
    }

    @Override
    public boolean forceRemoveAllMembers(Family family, User updatingUser)
    {
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, family.getDocumentReference())) {
            return false;
        }
        try {
            for (Patient patient : family.getMembers()) {
                // remove the member without updating family document (use "batch mode")
                // since we don't care about it as it will be removed anyway
                this.removeMember(family, patient, updatingUser, true);
            }
            return true;
        } catch (PTException ex) {
            this.logger.error("Failed to unlink all patients for the family [{}]: {}", family.getId(), ex.getMessage());
            return false;
        }
    }

    @Override
    public Family getFamilyById(String id)
    {
        if (StringUtils.isEmpty(id)) {
            return null;
        }
        DocumentReference reference = this.referenceResolver.resolve(id, Family.DATA_SPACE);
        XWikiContext context = this.provider.get();
        try {
            XWikiDocument familyDocument = context.getWiki().getDocument(reference, context);
            if (familyDocument.getXObject(Family.CLASS_REFERENCE) != null) {
                return new PhenotipsFamily(familyDocument);
            }
        } catch (XWikiException ex) {
            this.logger.error("Failed to load document for family [{}]: {}", id, ex.getMessage(), ex);
        }
        return null;
    }

    @Override
    /**
     * Returns a Family object for patient. If there's an XWiki family document but no PhenotipsFamily object associated
     * with it in the cache, a new PhenotipsFamily object will be created.
     *
     * @param patient for which to look for a family
     * @return Family if there's an XWiki family document, otherwise null
     */
    public Family getFamilyForPatient(Patient patient)
    {
        if (patient == null) {
            return null;
        }
        String patientId = patient.getId();
        XWikiDocument patientDocument = patient.getXDocument();
        if (patientDocument == null) {
            return null;
        }

        DocumentReference familyReference = getFamilyReference(patientDocument);
        if (familyReference == null) {
            this.logger.debug("Family not found for patient [{}]", patientId);
            return null;
        }

        try {
            XWikiDocument document = getDocument(familyReference);
            return new PhenotipsFamily(document);
        } catch (XWikiException e) {
            this.logger.error("Can't find family document for patient [{}]", patient.getId());
            return null;
        }
    }

    @Override
    public synchronized void addMember(Family family, Patient patient, User updatingUser) throws PTException
    {
        this.addMember(family, patient, updatingUser, false);
    }

    /**
     * This method may be called either as a standalone invocation, or internally as part of family pedigree update. The
     * latter invocation may add multiple patients (a "batch update")
     */
    private void addMember(Family family, Patient patient, User updatingUser, boolean batchUpdate) throws PTException
    {
        if (family == null) {
            throw new PTInvalidFamilyIdException(null);
        }
        if (patient == null) {
            throw new PTInvalidPatientIdException(null);
        }
        if (!batchUpdate) {
            // when called as part of a batch update all permissions have already been checked;
            // otherwise perform the check, which may throw some exceptiuon in case of problems
            this.checkIfPatientCanBeAddedToFamily(family, patient, updatingUser);
        }

        String patientId = patient.getId();
        XWikiContext context = this.provider.get();
        XWikiDocument patientDocument = patient.getXDocument();
        if (patientDocument == null) {
            throw new PTInvalidPatientIdException(patientId);
        }

        // Check if not already a member
        List<String> members = family.getMembersIds();
        if (members.contains(patientLinkString(patient))) {
            this.logger.error("Patient [{}] already a member of the same family, not adding", patientId);
            throw new PTPedigreeContainesSamePatientMultipleTimesException(patientId);
        }

        if (!this.setFamilyReference(patientDocument, family.getXDocument(), context)) {
            throw new PTInternalErrorException();
        }
        if (!savePatientDocument(patientDocument, "added to family " + family.getId(), context)) {
            throw new PTInternalErrorException();
        }

        // Add member to the list of family members
        members.add(patientLinkString(patient));
        BaseObject familyObject = family.getXDocument().getXObject(Family.CLASS_REFERENCE);
        familyObject.set(PhenotipsFamily.FAMILY_MEMBERS_FIELD, members, context);

        // only save family document if this add() is not performed as a part of a batch update
        if (!batchUpdate) {
            // updating permisisons is an expensive operation which takes all patients into account,
            // so don't do it when doing a bulk add or remove, instead the calling code will do one
            // update at the end
            this.updateFamilyPermissions(family, context, false);

            if (!saveFamilyDocument(family, "added " + patientId + " to the family", context)) {
                throw new PTInternalErrorException();
            }
        }
    }

    @Override
    public synchronized void removeMember(Family family, Patient patient, User updatingUser) throws PTException
    {
        this.removeMember(family, patient, updatingUser, false);
    }

    private void removeMember(Family family, Patient patient, User updatingUser, boolean batchUpdate)
        throws PTException
    {
        if (family == null) {
            throw new PTInvalidFamilyIdException(null);
        }
        if (patient == null) {
            throw new PTInvalidPatientIdException(null);
        }
        if (!batchUpdate) {
            // when called as part of a batch update all permissions have already been checked;
            // otherwise perform the check, which may throw some exceptiuon in case of problems
            this.checkIfPatientCanBeRemovedFromFamily(family, patient, updatingUser);
        }

        String patientId = patient.getId();
        XWikiContext context = this.provider.get();
        XWikiDocument patientDocument = patient.getXDocument();
        if (patientDocument == null) {
            throw new PTInvalidPatientIdException(patientId);
        }

        List<String> members = family.getMembersIds();
        if (!members.contains(patientLinkString(patient))) {
            this.logger.error("Can't remove patient [{}] from framily [{}]: patient not a member of the family",
                patientId, family.getId());
            throw new PTPatientNotInFamilyException(patientId);
        }

        // Remove reference to a family from patient document
        if (!this.removeFamilyReference(patientDocument)) {
            throw new PTInternalErrorException();
        }
        if (!savePatientDocument(patientDocument, "removed from family", context)) {
            throw new PTInternalErrorException();
        }

        // Remove patient from the pedigree
        Pedigree pedigree = family.getPedigree();
        if (pedigree != null) {
            pedigree.removeLink(patientId);
            if (!this.setPedigreeObject(family, pedigree, context)) {
                this.logger.error("Could not remove patient [{}] from pedigree from the family [{}]",
                    patientId, family.getId());
                throw new PTInternalErrorException();
            }
        }

        // Remove patient from family's members list
        members.remove(patientLinkString(patient));
        BaseObject familyObject = family.getXDocument().getXObject(Family.CLASS_REFERENCE);
        familyObject.set(PhenotipsFamily.FAMILY_MEMBERS_FIELD, members, context);

        if (!batchUpdate) {
            this.updateFamilyPermissions(family, context, false);

            if (!saveFamilyDocument(family, "removed " + patientId + " from the family", context)) {
                throw new PTInternalErrorException();
            }
        }
    }

    /**
     * Returns string as stored in the family members list.
     */
    private String patientLinkString(Patient patient)
    {
        return patient.getId();
    }

    @Override
    public synchronized void updateFamilyPermissions(Family family)
    {
        XWikiContext context = this.provider.get();
        this.updateFamilyPermissions(family, context, true);
    }

    private void updateFamilyPermissions(Family family, XWikiContext context, boolean saveXwikiDocument)
    {
        this.familyPermissions.updatePermissions(family, context);
        if (saveXwikiDocument) {
            this.saveFamilyDocument(family, "updated permissions", context);
        }
    }

    @Override
    public boolean canAddToFamily(Family family, Patient patient, User updatingUser, boolean throwException)
        throws PTException
    {
        try {
            if (family == null) {
                if (throwException) {
                    throw new PTInvalidFamilyIdException(null);
                }
                return false;
            }
            if (patient == null) {
                if (throwException) {
                    throw new PTInvalidPatientIdException(null);
                }
                return false;
            }
            this.checkIfPatientCanBeAddedToFamily(family, patient, updatingUser);
            return true;
        } catch (PTException ex) {
            if (throwException) {
                throw ex;
            }
            return false;
        }
    }

    @Override
    public boolean canDeleteFamily(Family family, User updatingUser,
        boolean deleteAllMembers, boolean throwException) throws PTException
    {
        try {
            if (family == null) {
                if (throwException) {
                    throw new PTInvalidFamilyIdException(null);
                }
                return false;
            }
            if (!this.authorizationService.hasAccess(updatingUser, Right.DELETE, family.getDocumentReference())) {
                throw new PTNotEnoughPermissionsOnFamilyException(Right.DELETE, family.getId());
            }
            if (deleteAllMembers) {
                // check permissions on all patients
                for (Patient patient : family.getMembers()) {
                    if (!this.authorizationService.hasAccess(
                            updatingUser, Right.DELETE, patient.getDocumentReference())) {
                        throw new PTNotEnoughPermissionsOnPatientException(Right.DELETE, patient.getId());
                    }
                }
            }
            return true;
        } catch (PTException ex) {
            if (throwException) {
                throw ex;
            }
            return false;
        }
    }

    private void checkIfPatientCanBeAddedToFamily(Family family, Patient patient, User updatingUser) throws PTException
    {
        // check rights
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, family.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnFamilyException(Right.EDIT, family.getId());
        }
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, patient.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnPatientException(Right.EDIT, patient.getId());
        }
        // check for logical problems: patient in another family
        Family familyForLinkedPatient = this.getFamilyForPatient(patient);
        if (familyForLinkedPatient != null && !familyForLinkedPatient.getId().equals(family.getId())) {
            throw new PTPatientAlreadyInAnotherFamilyException(patient.getId(), familyForLinkedPatient.getId());
        }
    }

    private void checkIfPatientCanBeRemovedFromFamily(Family family, Patient patient, User updatingUser)
        throws PTException
    {
        // check rights
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, family.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnFamilyException(Right.EDIT, family.getId());
        }
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, patient.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnPatientException(Right.EDIT, patient.getId());
        }
    }

    @Override
    public synchronized void setPedigree(Family family, Pedigree pedigree, User updatingUser) throws PTException
    {
        // note: whenever available, internal versions of helper methods are used which modify the
        // family document but do not save it to disk
        List<String> oldMembers = family.getMembersIds();

        List<String> currentMembers = pedigree.extractIds();

        // Add new members to family
        List<String> patientsToAdd = new LinkedList<>();
        patientsToAdd.addAll(currentMembers);
        patientsToAdd.removeAll(oldMembers);

        this.checkValidity(family, patientsToAdd, updatingUser);

        XWikiContext context = this.provider.get();
        context.setUserReference(updatingUser.getProfileDocument());
<commit_msg>PT-3385: Unauthenticated users can't save new pedigree Fixed<commit_after>/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/
 */
package org.phenotips.studies.family.internal;

import org.phenotips.Constants;
import org.phenotips.data.Patient;
import org.phenotips.data.PatientRepository;
import org.phenotips.data.permissions.Owner;
import org.phenotips.security.authorization.AuthorizationService;
import org.phenotips.studies.family.Family;
import org.phenotips.studies.family.FamilyRepository;
import org.phenotips.studies.family.Pedigree;
import org.phenotips.studies.family.PedigreeProcessor;
import org.phenotips.studies.family.exceptions.PTException;
import org.phenotips.studies.family.exceptions.PTInternalErrorException;
import org.phenotips.studies.family.exceptions.PTInvalidFamilyIdException;
import org.phenotips.studies.family.exceptions.PTInvalidPatientIdException;
import org.phenotips.studies.family.exceptions.PTNotEnoughPermissionsOnFamilyException;
import org.phenotips.studies.family.exceptions.PTNotEnoughPermissionsOnPatientException;
import org.phenotips.studies.family.exceptions.PTPatientAlreadyInAnotherFamilyException;
import org.phenotips.studies.family.exceptions.PTPatientNotInFamilyException;
import org.phenotips.studies.family.exceptions.PTPedigreeContainesSamePatientMultipleTimesException;

import org.xwiki.component.annotation.Component;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.query.Query;
import org.xwiki.query.QueryException;
import org.xwiki.query.QueryManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.users.User;

import java.util.LinkedList;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;

import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.slf4j.Logger;

import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;

/**
 * Provides utility methods for working with family documents and patients.
 *
 * @version $Id$
 * @since 1.4
 */
@Component(roles = FamilyRepository.class)
@Singleton
public class PhenotipsFamilyRepository implements FamilyRepository
{
    private static final String PREFIX = "FAM";

    private static final EntityReference FAMILY_TEMPLATE =
        new EntityReference("FamilyTemplate", EntityType.DOCUMENT, Constants.CODE_SPACE_REFERENCE);

    private static final String FAMILY_REFERENCE_FIELD = "reference";

    @Inject
    private Logger logger;

    @Inject
    private Provider<XWikiContext> provider;

    @Inject
    private PatientRepository patientRepository;

    @Inject
    private PhenotipsFamilyPermissions familyPermissions;

    @Inject
    private AuthorizationService authorizationService;

    @Inject
    private PedigreeProcessor pedigreeConverter;

    /** Runs queries for finding families. */
    @Inject
    private QueryManager qm;

    @Inject
    @Named("current")
    private DocumentReferenceResolver<String> referenceResolver;

    /** Fills in missing reference fields with those from the current context document to create a full reference. */
    @Inject
    @Named("current")
    private DocumentReferenceResolver<EntityReference> entityReferenceResolver;

    @Override
    public Family createFamily(User creator)
    {
        XWikiDocument newFamilyDocument = null;
        try {
            newFamilyDocument = this.createFamilyDocument(creator);
        } catch (Exception e) {
            this.logger.error("Could not create a new family document: {}", e.getMessage());
        }
        if (newFamilyDocument == null) {
            this.logger.debug("Could not create a family document");
            return null;
        }
        return new PhenotipsFamily(newFamilyDocument);
    }

    @Override
    public synchronized boolean deleteFamily(Family family, User updatingUser, boolean deleteAllMembers)
    {
        if (!canDeleteFamily(family, updatingUser, deleteAllMembers, false)) {
            return false;
        }
        if (deleteAllMembers) {
            for (Patient patient : family.getMembers()) {
                if (!this.patientRepository.delete(patient)) {
                    this.logger.error("Failed to delete patient [{}] - deletion of family [{}] aborted",
                        patient.getId(), family.getId());
                    return false;
                }
            }
        } else if (!this.forceRemoveAllMembers(family, updatingUser)) {
            return false;
        }

        try {
            XWikiContext context = this.provider.get();
            XWiki xwiki = context.getWiki();
            xwiki.deleteDocument(xwiki.getDocument(family.getXDocument(), context), context);
        } catch (XWikiException ex) {
            this.logger.error("Failed to delete family document [{}]: {}", family.getId(), ex.getMessage());
            return false;
        }
        return true;
    }

    @Override
    public boolean forceRemoveAllMembers(Family family, User updatingUser)
    {
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, family.getDocumentReference())) {
            return false;
        }
        try {
            for (Patient patient : family.getMembers()) {
                // remove the member without updating family document (use "batch mode")
                // since we don't care about it as it will be removed anyway
                this.removeMember(family, patient, updatingUser, true);
            }
            return true;
        } catch (PTException ex) {
            this.logger.error("Failed to unlink all patients for the family [{}]: {}", family.getId(), ex.getMessage());
            return false;
        }
    }

    @Override
    public Family getFamilyById(String id)
    {
        if (StringUtils.isEmpty(id)) {
            return null;
        }
        DocumentReference reference = this.referenceResolver.resolve(id, Family.DATA_SPACE);
        XWikiContext context = this.provider.get();
        try {
            XWikiDocument familyDocument = context.getWiki().getDocument(reference, context);
            if (familyDocument.getXObject(Family.CLASS_REFERENCE) != null) {
                return new PhenotipsFamily(familyDocument);
            }
        } catch (XWikiException ex) {
            this.logger.error("Failed to load document for family [{}]: {}", id, ex.getMessage(), ex);
        }
        return null;
    }

    @Override
    /**
     * Returns a Family object for patient. If there's an XWiki family document but no PhenotipsFamily object associated
     * with it in the cache, a new PhenotipsFamily object will be created.
     *
     * @param patient for which to look for a family
     * @return Family if there's an XWiki family document, otherwise null
     */
    public Family getFamilyForPatient(Patient patient)
    {
        if (patient == null) {
            return null;
        }
        String patientId = patient.getId();
        XWikiDocument patientDocument = patient.getXDocument();
        if (patientDocument == null) {
            return null;
        }

        DocumentReference familyReference = getFamilyReference(patientDocument);
        if (familyReference == null) {
            this.logger.debug("Family not found for patient [{}]", patientId);
            return null;
        }

        try {
            XWikiDocument document = getDocument(familyReference);
            return new PhenotipsFamily(document);
        } catch (XWikiException e) {
            this.logger.error("Can't find family document for patient [{}]", patient.getId());
            return null;
        }
    }

    @Override
    public synchronized void addMember(Family family, Patient patient, User updatingUser) throws PTException
    {
        this.addMember(family, patient, updatingUser, false);
    }

    /**
     * This method may be called either as a standalone invocation, or internally as part of family pedigree update. The
     * latter invocation may add multiple patients (a "batch update")
     */
    private void addMember(Family family, Patient patient, User updatingUser, boolean batchUpdate) throws PTException
    {
        if (family == null) {
            throw new PTInvalidFamilyIdException(null);
        }
        if (patient == null) {
            throw new PTInvalidPatientIdException(null);
        }
        if (!batchUpdate) {
            // when called as part of a batch update all permissions have already been checked;
            // otherwise perform the check, which may throw some exceptiuon in case of problems
            this.checkIfPatientCanBeAddedToFamily(family, patient, updatingUser);
        }

        String patientId = patient.getId();
        XWikiContext context = this.provider.get();
        XWikiDocument patientDocument = patient.getXDocument();
        if (patientDocument == null) {
            throw new PTInvalidPatientIdException(patientId);
        }

        // Check if not already a member
        List<String> members = family.getMembersIds();
        if (members.contains(patientLinkString(patient))) {
            this.logger.error("Patient [{}] already a member of the same family, not adding", patientId);
            throw new PTPedigreeContainesSamePatientMultipleTimesException(patientId);
        }

        if (!this.setFamilyReference(patientDocument, family.getXDocument(), context)) {
            throw new PTInternalErrorException();
        }
        if (!savePatientDocument(patientDocument, "added to family " + family.getId(), context)) {
            throw new PTInternalErrorException();
        }

        // Add member to the list of family members
        members.add(patientLinkString(patient));
        BaseObject familyObject = family.getXDocument().getXObject(Family.CLASS_REFERENCE);
        familyObject.set(PhenotipsFamily.FAMILY_MEMBERS_FIELD, members, context);

        // only save family document if this add() is not performed as a part of a batch update
        if (!batchUpdate) {
            // updating permisisons is an expensive operation which takes all patients into account,
            // so don't do it when doing a bulk add or remove, instead the calling code will do one
            // update at the end
            this.updateFamilyPermissions(family, context, false);

            if (!saveFamilyDocument(family, "added " + patientId + " to the family", context)) {
                throw new PTInternalErrorException();
            }
        }
    }

    @Override
    public synchronized void removeMember(Family family, Patient patient, User updatingUser) throws PTException
    {
        this.removeMember(family, patient, updatingUser, false);
    }

    private void removeMember(Family family, Patient patient, User updatingUser, boolean batchUpdate)
        throws PTException
    {
        if (family == null) {
            throw new PTInvalidFamilyIdException(null);
        }
        if (patient == null) {
            throw new PTInvalidPatientIdException(null);
        }
        if (!batchUpdate) {
            // when called as part of a batch update all permissions have already been checked;
            // otherwise perform the check, which may throw some exceptiuon in case of problems
            this.checkIfPatientCanBeRemovedFromFamily(family, patient, updatingUser);
        }

        String patientId = patient.getId();
        XWikiContext context = this.provider.get();
        XWikiDocument patientDocument = patient.getXDocument();
        if (patientDocument == null) {
            throw new PTInvalidPatientIdException(patientId);
        }

        List<String> members = family.getMembersIds();
        if (!members.contains(patientLinkString(patient))) {
            this.logger.error("Can't remove patient [{}] from framily [{}]: patient not a member of the family",
                patientId, family.getId());
            throw new PTPatientNotInFamilyException(patientId);
        }

        // Remove reference to a family from patient document
        if (!this.removeFamilyReference(patientDocument)) {
            throw new PTInternalErrorException();
        }
        if (!savePatientDocument(patientDocument, "removed from family", context)) {
            throw new PTInternalErrorException();
        }

        // Remove patient from the pedigree
        Pedigree pedigree = family.getPedigree();
        if (pedigree != null) {
            pedigree.removeLink(patientId);
            if (!this.setPedigreeObject(family, pedigree, context)) {
                this.logger.error("Could not remove patient [{}] from pedigree from the family [{}]",
                    patientId, family.getId());
                throw new PTInternalErrorException();
            }
        }

        // Remove patient from family's members list
        members.remove(patientLinkString(patient));
        BaseObject familyObject = family.getXDocument().getXObject(Family.CLASS_REFERENCE);
        familyObject.set(PhenotipsFamily.FAMILY_MEMBERS_FIELD, members, context);

        if (!batchUpdate) {
            this.updateFamilyPermissions(family, context, false);

            if (!saveFamilyDocument(family, "removed " + patientId + " from the family", context)) {
                throw new PTInternalErrorException();
            }
        }
    }

    /**
     * Returns string as stored in the family members list.
     */
    private String patientLinkString(Patient patient)
    {
        return patient.getId();
    }

    @Override
    public synchronized void updateFamilyPermissions(Family family)
    {
        XWikiContext context = this.provider.get();
        this.updateFamilyPermissions(family, context, true);
    }

    private void updateFamilyPermissions(Family family, XWikiContext context, boolean saveXwikiDocument)
    {
        this.familyPermissions.updatePermissions(family, context);
        if (saveXwikiDocument) {
            this.saveFamilyDocument(family, "updated permissions", context);
        }
    }

    @Override
    public boolean canAddToFamily(Family family, Patient patient, User updatingUser, boolean throwException)
        throws PTException
    {
        try {
            if (family == null) {
                if (throwException) {
                    throw new PTInvalidFamilyIdException(null);
                }
                return false;
            }
            if (patient == null) {
                if (throwException) {
                    throw new PTInvalidPatientIdException(null);
                }
                return false;
            }
            this.checkIfPatientCanBeAddedToFamily(family, patient, updatingUser);
            return true;
        } catch (PTException ex) {
            if (throwException) {
                throw ex;
            }
            return false;
        }
    }

    @Override
    public boolean canDeleteFamily(Family family, User updatingUser,
        boolean deleteAllMembers, boolean throwException) throws PTException
    {
        try {
            if (family == null) {
                if (throwException) {
                    throw new PTInvalidFamilyIdException(null);
                }
                return false;
            }
            if (!this.authorizationService.hasAccess(updatingUser, Right.DELETE, family.getDocumentReference())) {
                throw new PTNotEnoughPermissionsOnFamilyException(Right.DELETE, family.getId());
            }
            if (deleteAllMembers) {
                // check permissions on all patients
                for (Patient patient : family.getMembers()) {
                    if (!this.authorizationService.hasAccess(
                            updatingUser, Right.DELETE, patient.getDocumentReference())) {
                        throw new PTNotEnoughPermissionsOnPatientException(Right.DELETE, patient.getId());
                    }
                }
            }
            return true;
        } catch (PTException ex) {
            if (throwException) {
                throw ex;
            }
            return false;
        }
    }

    private void checkIfPatientCanBeAddedToFamily(Family family, Patient patient, User updatingUser) throws PTException
    {
        // check rights
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, family.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnFamilyException(Right.EDIT, family.getId());
        }
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, patient.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnPatientException(Right.EDIT, patient.getId());
        }
        // check for logical problems: patient in another family
        Family familyForLinkedPatient = this.getFamilyForPatient(patient);
        if (familyForLinkedPatient != null && !familyForLinkedPatient.getId().equals(family.getId())) {
            throw new PTPatientAlreadyInAnotherFamilyException(patient.getId(), familyForLinkedPatient.getId());
        }
    }

    private void checkIfPatientCanBeRemovedFromFamily(Family family, Patient patient, User updatingUser)
        throws PTException
    {
        // check rights
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, family.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnFamilyException(Right.EDIT, family.getId());
        }
        if (!this.authorizationService.hasAccess(updatingUser, Right.EDIT, patient.getDocumentReference())) {
            throw new PTNotEnoughPermissionsOnPatientException(Right.EDIT, patient.getId());
        }
    }

    @Override
    public synchronized void setPedigree(Family family, Pedigree pedigree, User updatingUser) throws PTException
    {
        // note: whenever available, internal versions of helper methods are used which modify the
        // family document but do not save it to disk
        List<String> oldMembers = family.getMembersIds();

        List<String> currentMembers = pedigree.extractIds();

        // Add new members to family
        List<String> patientsToAdd = new LinkedList<>();
        patientsToAdd.addAll(currentMembers);
        patientsToAdd.removeAll(oldMembers);

        this.checkValidity(family, patientsToAdd, updatingUser);

        XWikiContext context = this.provider.get();
        context.setUserReference(updatingUser == null ? null : updatingUser.getProfileDocument());

{'fraction_non_alphanumeric': 0.08509596805358753, 'fraction_numerical': 0.0003606853020739405, 'mean_word_length': 2.617858141485693, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4338589', 'n_tokens_mistral': 9837, 'n_tokens_neox': 9542, 'n_words': 2668}