<commit_before>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/internal-plugin-encryption';
import '@ciscospark/internal-plugin-conversation';

import {registerInternalPlugin} from '@ciscospark/spark-core';
import {has} from 'lodash';

import EDiscovery from './ediscovery';
import {InvalidEmailAddressError} from './ediscovery-error';

registerInternalPlugin('ediscovery', EDiscovery, {
  payloadTransformer: {
    predicates: [
      {
        name: 'decryptReportRequest',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.reportRequest'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportRequestArray',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body[0].reportRequest'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'transformReportRequestEmailsToUUIDS',
        direction: 'outbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.emails'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'encryptReportRequest',
        direction: 'outbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.keywords'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContent',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.activityId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContentArray',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body[0].activityId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContentSummary',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.spaceId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContentSummaryArray',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body[0].spaceId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      }
    ],
    transforms: [
      {
        name: 'decryptReportRequest',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body || !object.body.reportRequest || !object.body.reportRequest.encryptionKeyUrl) {
            return Promise.resolve();
          }
          const {reportRequest} = object.body;
          let spaceNamePromises = [];

          if (reportRequest.spaceNames) {
            spaceNamePromises = Promise.all(reportRequest.spaceNames.map((spaceName) => ctx.spark.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, spaceName)))
              .then((decryptedSpaceNames) => {
                reportRequest.spaceNames = decryptedSpaceNames;
              });
          }

          let keywordPromises = [];

          if (reportRequest.keywords) {
            keywordPromises = Promise.all(reportRequest.keywords.map((keyword) => ctx.spark.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, keyword)))
              .then((decryptedKeywords) => {
                reportRequest.keywords = decryptedKeywords;
              });
          }

          let emailPromises = [];

          if (reportRequest.emails) {
            emailPromises = Promise.all(reportRequest.emails.map((email) => ctx.spark.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, email)))
              .then((decryptedEmails) => {
                reportRequest.emails = decryptedEmails;
              });
          }

          return Promise.all([
            ctx.transform('decryptTextProp', 'name', reportRequest.encryptionKeyUrl, reportRequest),
            ctx.transform('decryptTextProp', 'description', reportRequest.encryptionKeyUrl, reportRequest)
          ].concat(spaceNamePromises, keywordPromises, emailPromises));
        }
      },
      {
        name: 'decryptReportRequestArray',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }

          return Promise.all(object.body.map((item) => ctx.transform('decryptReportRequest', {body: item})));
        }
      },
      {
        name: 'transformReportRequestEmailsToUUIDS',
        direction: 'outbound',
        fn(ctx, object) {
          if (!object || !object.body || !object.body.emails || object.body.emails.length <= 0) {
            return Promise.resolve();
          }
          const reportRequest = object.body;

          const promises = [];
          const invalidEmails = [];

          reportRequest.emails.forEach((userEmail) => {
            promises.push(
              // The people list API only supports a single email
              ctx.spark.people.list({email: userEmail})
                .then((res) => {
                  let uuid = '';

                  if (res.items.length <= 0) {
                    ctx.spark.logger.error(`Unable to find user with email ${userEmail}`);
                    invalidEmails.push(userEmail);
                  }
                  else {
                    // User id is base64 encoded and of the format Y2lzY29zcGFyazovL3VzL1BFT1BMRS81ZDU5Yjc5NS02ZmEyLTQ2NTQtOGVjMi03NjlkYjE1YzBkOWU
                    const decodedId = Buffer.from(res.items[0].id, 'base64').toString();

                    // Decode and strip out the uuid ciscospark://us/PEOPLE/5d59b795-6fa2-4654-8ec2-769db15c0d9e
                    uuid = decodedId.substring(decodedId.lastIndexOf('/') + 1, decodedId.length);
                  }

                  return uuid;
                })
                .catch((reason) => Promise.reject(reason))
            );
          });

          return Promise.all(promises)
            .then((uuids) => {
              if (invalidEmails.length) {
                return Promise.reject(new InvalidEmailAddressError(invalidEmails));
              }
              reportRequest.userIds = uuids;

              return Promise.resolve();
            });
        }
      },
      {
        name: 'encryptReportRequest',
        direction: 'outbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }
          const reportRequest = object.body;

          return ctx.spark.internal.encryption.kms.createUnboundKeys({count: 1})
            .then((keys) => {
              if (keys && keys.length > 0 && keys[0]) {
                reportRequest.encryptionKeyUrl = keys[0].uri;

                return ctx.spark.internal.encryption.kms.createResource({userIds: [keys[0].userId], keys})
                  .then(() => {
                    const spaceNamePromises = Promise.all(reportRequest.spaceNames.map((spaceName) => ctx.spark.internal.encryption.encryptText(keys[0], spaceName)))
                      .then((encryptedSpaceNames) => {
                        reportRequest.spaceNames = encryptedSpaceNames;
                      });

                    const keywordPromises = Promise.all(reportRequest.keywords.map((keyword) => ctx.spark.internal.encryption.encryptText(keys[0], keyword)))
                      .then((encryptedKeywords) => {
                        reportRequest.keywords = encryptedKeywords;
                      });

                    const emailPromises = Promise.all(reportRequest.emails.map((email) => ctx.spark.internal.encryption.encryptText(keys[0], email)))
                      .then((encryptedEmails) => {
                        reportRequest.emails = encryptedEmails;
                      });

                    return Promise.all([
                      ctx.transform('encryptTextProp', 'name', keys[0], reportRequest),
                      ctx.transform('encryptTextProp', 'description', keys[0], reportRequest)
                    ].concat(spaceNamePromises, keywordPromises, emailPromises));
                  });
              }

              return Promise.resolve();
            })
            .catch((reason) => Promise.reject(reason));
        }
      },
      {
        name: 'decryptReportContent',
        direction: 'inbound',
        fn(ctx, object, reportId) {
          if (!object || !object.body || !reportId) {
            return Promise.resolve();
          }
          const activity = object.body;

          const promises = [];

          return ctx.spark.internal.ediscovery.getContentSummaryBySpaceId(reportId, activity.targetId)
            .then((spaceSummary) => {
              if (!spaceSummary) {
                ctx.spark.logger.error(`Undefined space summary for activity ${activity.activityId}`);

                return object;
              }

              // set space name and participants on activity
              if (spaceSummary.spaceName) {
                activity.spaceName = spaceSummary.spaceName;
              }

              if (spaceSummary.participantDisplayNames) {
                // Remove sender from list of participants as they will appear in the 'To' field
                activity.participantDisplayNames = spaceSummary.participantDisplayNames.filter((value) => value !== activity.actorDisplayName).toString();
                if (spaceSummary.isOneOnOne) {
                  // One to One spaces have no space name, use participant names as 'Subject' instead
                  activity.spaceName = spaceSummary.participantDisplayNames.toString().replace(',', ' & ');
                }
              }

              // decrypt content
              let spaceParticipantId;

              if (spaceSummary.participants && spaceSummary.participants.length) {
                spaceParticipantId = spaceSummary.participants[0];
              }
              else {
                // TODO: Resolve with assistance from KMS team
                ctx.spark.logger.error(`Known error - detected space with no participant - unable to decrypt activity ${activity.activityId}`);

                return activity;
              }

              // Decrypt activity message if present
              // For 'add' activities the objectDisplayName is a user id which does not need to be decrypted
              if (activity.verb !== 'add' && activity.objectDisplayName && activity.encryptionKeyUrl) {
                promises.push(ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, activity.objectDisplayName, {onBehalfOf: spaceParticipantId})
                  .then((decryptedMessage) => {
                    activity.objectDisplayName = decryptedMessage;
                  })
                  .catch((reason) => {
                    ctx.spark.logger.error(`Decrypt message error for activity ${activity.activityId}: ${reason}`);

                    return object; // TODO - determine correct behaviour when an individual operation fails
                  }));
              }

              // Decrypt shares (files, whiteboards, shared links)
              // Array.prototype.concat.apply ignores undefined
              let shares = Array.prototype.concat.apply([], activity.files);

              shares = Array.prototype.concat.apply(shares, activity.whiteboards);
              shares = Array.prototype.concat.apply(shares, activity.links);
              for (let i = 0; i < shares.length; i += 1) {
                const share = shares[i];
                // Decrypt the share's display name
                // Ignore display names for whiteboards which are unencrypted
                // Ignore the unencrypted 'NoName' display name the ED service sets in error situations

                if (share.displayName && (!activity.whiteboards || !activity.whiteboards.includes(share)) && share.displayName !== 'NoName') {
                  promises.push(
                    ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, share.displayName, {onBehalfOf: spaceParticipantId})
                      .then((decryptedDisplayName) => {
                        share.displayName = decryptedDisplayName;
                      })
                      .catch((reason) => {
                        ctx.spark.logger.error(`Decrypt DisplayName error for activity ${activity.activityId} and share ${share}: ${reason}`);

                        return object; // TODO - determine correct behaviour when an individual operation fails
                      })
                  );
                }

                // Shared Links can have additional decryption fields
                if (share.microsoftSharedLinkInfo) {
                  if (share.microsoftSharedLinkInfo.driveId) {
                    promises.push(
                      ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, share.microsoftSharedLinkInfo.driveId, {onBehalfOf: spaceParticipantId})
                        .then((decryptedDriveId) => {
                          share.microsoftSharedLinkInfo.driveId = decryptedDriveId;
                        })
                        .catch((reason) => {
                          ctx.spark.logger.error(`Decrypt share.microsoftSharedLinkInfo.driveId error for activity ${activity.activityId} and share ${share}: ${reason}`);

                          return object; // TODO - determine correct behaviour when an individual operation fails
                        })
                    );
                  }

                  if (share.microsoftSharedLinkInfo.itemId) {
                    promises.push(
                      ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, share.microsoftSharedLinkInfo.itemId, {onBehalfOf: spaceParticipantId})
                        .then((decryptedItemId) => {
                          share.microsoftSharedLinkInfo.itemId = decryptedItemId;
                        })
                        .catch((reason) => {
                          ctx.spark.logger.error(`Decrypt share.microsoftSharedLinkInfo.itemId error for activity ${activity.activityId} and share ${share}: ${reason}`);

                          return object; // TODO - determine correct behaviour when an individual operation fails
                        })
                    );
                  }
                }

                // Decrypt the scr (Secure Content Reference) or sslr (Secure Shared Link Reference)
                // Unlike a scr the sslr contains only a loc. But decryptScr(...) is flexible and
                // leaves the tag, auth, IV, etc fields on the SCR object as undefined.
                if (share.scr || share.sslr) {
                  promises.push(
                    ctx.spark.internal.encryption.decryptScr(activity.encryptionKeyUrl, share.scr || share.sslr, {onBehalfOf: spaceParticipantId})
                      .then((decryptedSCR) => {
                        if (share.scr) {
                          share.scr = decryptedSCR;
                        }
                        else {
                          share.sslr = decryptedSCR.loc;
                        }
                      })
                      .catch((reason) => {
                        ctx.spark.logger.error(`Decrypt file scr or sslr error for activity ${activity.activityId} and share ${share}: ${reason}`);

                        return object; // TODO - determine correct behaviour when an individual operation fails
                      })
                  );
                }
              }

              return Promise.all(promises);
            })
            .catch((reason) => {
              ctx.spark.logger.error(`Error retrieving space summary for: ${object.activityId}: ${reason}`);

              return object;
            });
        }
      },
      {
        name: 'decryptReportContentArray',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }
          // the report id is needed in order to retrieve space information for activities with whiteboards so that
          // the list of users in those spaces can be accessed and the content can be decrypted on their behalf
          const reportId = object.options.resource.split('/')[1];

          return Promise.all(object.body.map((item) => ctx.transform('decryptReportContent', {body: item}, reportId)));
        }
      },
      {
        name: 'decryptReportContentSummary',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body || !object.body.spaceName || !object.body.participants || !object.body.participants.length) {
            return Promise.resolve();
          }
          const spaceSummary = object.body;

          return ctx.spark.internal.encryption.decryptText(spaceSummary.encryptionKeyUrl, spaceSummary.spaceName, {onBehalfOf: spaceSummary.participants[0]})
            .then((decryptedSpaceName) => {
              spaceSummary.spaceName = decryptedSpaceName;
            })
            .catch((reason) => {
              ctx.spark.logger.error(`Decrypt space name error for space ${spaceSummary.spaceId}: ${reason}`);

              return object; // TODO - determine correct behaviour when an individual operation fails
            });
        }
      },
      {
        name: 'decryptReportContentSummaryArray',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }

          return Promise.all(object.body.map((item) => ctx.transform('decryptReportContentSummary', {body: item})));
        }
      }
    ]
  }
});

export default EDiscovery;

export {config} from './config';
export {default as ReportRequest} from './report-request';
export {EdiscoveryError, InvalidEmailAddressError} from './ediscovery-error';
<commit_msg>fix(ediscovery): encrypt when body.keywords is not set<commit_after>/*!
 * Copyright (c) 2015-2017 Cisco Systems, Inc. See LICENSE file.
 */

import '@ciscospark/internal-plugin-encryption';
import '@ciscospark/internal-plugin-conversation';

import {registerInternalPlugin} from '@ciscospark/spark-core';
import {has} from 'lodash';

import EDiscovery from './ediscovery';
import {InvalidEmailAddressError} from './ediscovery-error';

registerInternalPlugin('ediscovery', EDiscovery, {
  payloadTransformer: {
    predicates: [
      {
        name: 'decryptReportRequest',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.reportRequest'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportRequestArray',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body[0].reportRequest'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'transformReportRequestEmailsToUUIDS',
        direction: 'outbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.emails'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'encryptReportRequest',
        direction: 'outbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.keywords') || has(object, 'body.spaceNames') || has(object, 'body.emails'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContent',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.activityId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContentArray',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body[0].activityId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContentSummary',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body.spaceId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      },
      {
        name: 'decryptReportContentSummaryArray',
        direction: 'inbound',
        test(ctx, object) {
          return Promise.resolve(has(object, 'body[0].spaceId'));
        },
        extract(object) {
          return Promise.resolve(object);
        }
      }
    ],
    transforms: [
      {
        name: 'decryptReportRequest',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body || !object.body.reportRequest || !object.body.reportRequest.encryptionKeyUrl) {
            return Promise.resolve();
          }
          const {reportRequest} = object.body;
          let spaceNamePromises = [];

          if (reportRequest.spaceNames) {
            spaceNamePromises = Promise.all(reportRequest.spaceNames.map((spaceName) => ctx.spark.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, spaceName)))
              .then((decryptedSpaceNames) => {
                reportRequest.spaceNames = decryptedSpaceNames;
              });
          }

          let keywordPromises = [];

          if (reportRequest.keywords) {
            keywordPromises = Promise.all(reportRequest.keywords.map((keyword) => ctx.spark.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, keyword)))
              .then((decryptedKeywords) => {
                reportRequest.keywords = decryptedKeywords;
              });
          }

          let emailPromises = [];

          if (reportRequest.emails) {
            emailPromises = Promise.all(reportRequest.emails.map((email) => ctx.spark.internal.encryption.decryptText(reportRequest.encryptionKeyUrl, email)))
              .then((decryptedEmails) => {
                reportRequest.emails = decryptedEmails;
              });
          }

          return Promise.all([
            ctx.transform('decryptTextProp', 'name', reportRequest.encryptionKeyUrl, reportRequest),
            ctx.transform('decryptTextProp', 'description', reportRequest.encryptionKeyUrl, reportRequest)
          ].concat(spaceNamePromises, keywordPromises, emailPromises));
        }
      },
      {
        name: 'decryptReportRequestArray',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }

          return Promise.all(object.body.map((item) => ctx.transform('decryptReportRequest', {body: item})));
        }
      },
      {
        name: 'transformReportRequestEmailsToUUIDS',
        direction: 'outbound',
        fn(ctx, object) {
          if (!object || !object.body || !object.body.emails || object.body.emails.length <= 0) {
            return Promise.resolve();
          }
          const reportRequest = object.body;

          const promises = [];
          const invalidEmails = [];

          reportRequest.emails.forEach((userEmail) => {
            promises.push(
              // The people list API only supports a single email
              ctx.spark.people.list({email: userEmail})
                .then((res) => {
                  let uuid = '';

                  if (res.items.length <= 0) {
                    ctx.spark.logger.error(`Unable to find user with email ${userEmail}`);
                    invalidEmails.push(userEmail);
                  }
                  else {
                    // User id is base64 encoded and of the format Y2lzY29zcGFyazovL3VzL1BFT1BMRS81ZDU5Yjc5NS02ZmEyLTQ2NTQtOGVjMi03NjlkYjE1YzBkOWU
                    const decodedId = Buffer.from(res.items[0].id, 'base64').toString();

                    // Decode and strip out the uuid ciscospark://us/PEOPLE/5d59b795-6fa2-4654-8ec2-769db15c0d9e
                    uuid = decodedId.substring(decodedId.lastIndexOf('/') + 1, decodedId.length);
                  }

                  return uuid;
                })
                .catch((reason) => Promise.reject(reason))
            );
          });

          return Promise.all(promises)
            .then((uuids) => {
              if (invalidEmails.length) {
                return Promise.reject(new InvalidEmailAddressError(invalidEmails));
              }
              reportRequest.userIds = uuids;

              return Promise.resolve();
            });
        }
      },
      {
        name: 'encryptReportRequest',
        direction: 'outbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }
          const reportRequest = object.body;

          return ctx.spark.internal.encryption.kms.createUnboundKeys({count: 1})
            .then((keys) => {
              if (keys && keys.length > 0 && keys[0]) {
                reportRequest.encryptionKeyUrl = keys[0].uri;

                return ctx.spark.internal.encryption.kms.createResource({userIds: [keys[0].userId], keys})
                  .then(() => {
                    const promises = [];

                    if (reportRequest.spaceNames) {
                      promises.push(
                        Promise.all(reportRequest.spaceNames.map((spaceName) => ctx.spark.internal.encryption.encryptText(keys[0], spaceName)))
                          .then((encryptedSpaceNames) => {
                            reportRequest.spaceNames = encryptedSpaceNames;
                          })
                      );
                    }

                    if (reportRequest.keywords) {
                      promises.push(
                        Promise.all(reportRequest.keywords.map((keyword) => ctx.spark.internal.encryption.encryptText(keys[0], keyword)))
                          .then((encryptedKeywords) => {
                            reportRequest.keywords = encryptedKeywords;
                          })
                      );
                    }

                    if (reportRequest.emails) {
                      promises.push(
                        Promise.all(reportRequest.emails.map((email) => ctx.spark.internal.encryption.encryptText(keys[0], email)))
                          .then((encryptedEmails) => {
                            reportRequest.emails = encryptedEmails;
                          })
                      );
                    }

                    promises.push(ctx.transform('encryptTextProp', 'name', keys[0], reportRequest));
                    promises.push(ctx.transform('encryptTextProp', 'description', keys[0], reportRequest));

                    return Promise.all(promises);
                  });
              }

              return Promise.resolve();
            })
            .catch((reason) => Promise.reject(reason));
        }
      },
      {
        name: 'decryptReportContent',
        direction: 'inbound',
        fn(ctx, object, reportId) {
          if (!object || !object.body || !reportId) {
            return Promise.resolve();
          }
          const activity = object.body;

          const promises = [];

          return ctx.spark.internal.ediscovery.getContentSummaryBySpaceId(reportId, activity.targetId)
            .then((spaceSummary) => {
              if (!spaceSummary) {
                ctx.spark.logger.error(`Undefined space summary for activity ${activity.activityId}`);

                return object;
              }

              // set space name and participants on activity
              if (spaceSummary.spaceName) {
                activity.spaceName = spaceSummary.spaceName;
              }

              if (spaceSummary.participantDisplayNames) {
                // Remove sender from list of participants as they will appear in the 'To' field
                activity.participantDisplayNames = spaceSummary.participantDisplayNames.filter((value) => value !== activity.actorDisplayName).toString();
                if (spaceSummary.isOneOnOne) {
                  // One to One spaces have no space name, use participant names as 'Subject' instead
                  activity.spaceName = spaceSummary.participantDisplayNames.toString().replace(',', ' & ');
                }
              }

              // decrypt content
              let spaceParticipantId;

              if (spaceSummary.participants && spaceSummary.participants.length) {
                spaceParticipantId = spaceSummary.participants[0];
              }
              else {
                // TODO: Resolve with assistance from KMS team
                ctx.spark.logger.error(`Known error - detected space with no participant - unable to decrypt activity ${activity.activityId}`);

                return activity;
              }

              // Decrypt activity message if present
              // For 'add' activities the objectDisplayName is a user id which does not need to be decrypted
              if (activity.verb !== 'add' && activity.objectDisplayName && activity.encryptionKeyUrl) {
                promises.push(ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, activity.objectDisplayName, {onBehalfOf: spaceParticipantId})
                  .then((decryptedMessage) => {
                    activity.objectDisplayName = decryptedMessage;
                  })
                  .catch((reason) => {
                    ctx.spark.logger.error(`Decrypt message error for activity ${activity.activityId}: ${reason}`);

                    return object; // TODO - determine correct behaviour when an individual operation fails
                  }));
              }

              // Decrypt shares (files, whiteboards, shared links)
              // Array.prototype.concat.apply ignores undefined
              let shares = Array.prototype.concat.apply([], activity.files);

              shares = Array.prototype.concat.apply(shares, activity.whiteboards);
              shares = Array.prototype.concat.apply(shares, activity.links);
              for (let i = 0; i < shares.length; i += 1) {
                const share = shares[i];
                // Decrypt the share's display name
                // Ignore display names for whiteboards which are unencrypted
                // Ignore the unencrypted 'NoName' display name the ED service sets in error situations

                if (share.displayName && (!activity.whiteboards || !activity.whiteboards.includes(share)) && share.displayName !== 'NoName') {
                  promises.push(
                    ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, share.displayName, {onBehalfOf: spaceParticipantId})
                      .then((decryptedDisplayName) => {
                        share.displayName = decryptedDisplayName;
                      })
                      .catch((reason) => {
                        ctx.spark.logger.error(`Decrypt DisplayName error for activity ${activity.activityId} and share ${share}: ${reason}`);

                        return object; // TODO - determine correct behaviour when an individual operation fails
                      })
                  );
                }

                // Shared Links can have additional decryption fields
                if (share.microsoftSharedLinkInfo) {
                  if (share.microsoftSharedLinkInfo.driveId) {
                    promises.push(
                      ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, share.microsoftSharedLinkInfo.driveId, {onBehalfOf: spaceParticipantId})
                        .then((decryptedDriveId) => {
                          share.microsoftSharedLinkInfo.driveId = decryptedDriveId;
                        })
                        .catch((reason) => {
                          ctx.spark.logger.error(`Decrypt share.microsoftSharedLinkInfo.driveId error for activity ${activity.activityId} and share ${share}: ${reason}`);

                          return object; // TODO - determine correct behaviour when an individual operation fails
                        })
                    );
                  }

                  if (share.microsoftSharedLinkInfo.itemId) {
                    promises.push(
                      ctx.spark.internal.encryption.decryptText(activity.encryptionKeyUrl, share.microsoftSharedLinkInfo.itemId, {onBehalfOf: spaceParticipantId})
                        .then((decryptedItemId) => {
                          share.microsoftSharedLinkInfo.itemId = decryptedItemId;
                        })
                        .catch((reason) => {
                          ctx.spark.logger.error(`Decrypt share.microsoftSharedLinkInfo.itemId error for activity ${activity.activityId} and share ${share}: ${reason}`);

                          return object; // TODO - determine correct behaviour when an individual operation fails
                        })
                    );
                  }
                }

                // Decrypt the scr (Secure Content Reference) or sslr (Secure Shared Link Reference)
                // Unlike a scr the sslr contains only a loc. But decryptScr(...) is flexible and
                // leaves the tag, auth, IV, etc fields on the SCR object as undefined.
                if (share.scr || share.sslr) {
                  promises.push(
                    ctx.spark.internal.encryption.decryptScr(activity.encryptionKeyUrl, share.scr || share.sslr, {onBehalfOf: spaceParticipantId})
                      .then((decryptedSCR) => {
                        if (share.scr) {
                          share.scr = decryptedSCR;
                        }
                        else {
                          share.sslr = decryptedSCR.loc;
                        }
                      })
                      .catch((reason) => {
                        ctx.spark.logger.error(`Decrypt file scr or sslr error for activity ${activity.activityId} and share ${share}: ${reason}`);

                        return object; // TODO - determine correct behaviour when an individual operation fails
                      })
                  );
                }
              }

              return Promise.all(promises);
            })
            .catch((reason) => {
              ctx.spark.logger.error(`Error retrieving space summary for: ${object.activityId}: ${reason}`);

              return object;
            });
        }
      },
      {
        name: 'decryptReportContentArray',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }
          // the report id is needed in order to retrieve space information for activities with whiteboards so that
          // the list of users in those spaces can be accessed and the content can be decrypted on their behalf
          const reportId = object.options.resource.split('/')[1];

          return Promise.all(object.body.map((item) => ctx.transform('decryptReportContent', {body: item}, reportId)));
        }
      },
      {
        name: 'decryptReportContentSummary',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body || !object.body.spaceName || !object.body.participants || !object.body.participants.length) {
            return Promise.resolve();
          }
          const spaceSummary = object.body;

          return ctx.spark.internal.encryption.decryptText(spaceSummary.encryptionKeyUrl, spaceSummary.spaceName, {onBehalfOf: spaceSummary.participants[0]})
            .then((decryptedSpaceName) => {
              spaceSummary.spaceName = decryptedSpaceName;
            })
            .catch((reason) => {
              ctx.spark.logger.error(`Decrypt space name error for space ${spaceSummary.spaceId}: ${reason}`);

              return object; // TODO - determine correct behaviour when an individual operation fails
            });
        }
      },
      {
        name: 'decryptReportContentSummaryArray',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object || !object.body) {
            return Promise.resolve();
          }

          return Promise.all(object.body.map((item) => ctx.transform('decryptReportContentSummary', {body: item})));
        }
      }
    ]
  }
});

export default EDiscovery;

export {config} from './config';
export {default as ReportRequest} from './report-request';
export {EdiscoveryError, InvalidEmailAddressError} from './ediscovery-error';
<|endoftext|>


<commit_before>import { createElement, Component } from 'react';

const PAGINATION = {
  TYPE: {
    OFFSET_AND_LIMIT: 'offsetAndLimit',
    CURSOR: 'cursor'
  },
  MODE: {
    INFINITE: 'infinite'
  },
  PRESET: {
    infiniteOffsetAndLimit: (initialSize, nextSize = initialSize) => ({
      type: PAGINATION.TYPE.OFFSET_AND_LIMIT,
      mode: PAGINATION.MODE.INFINITE,
      getNextPage: ({ limit, offset }) => {
        if (offset === null) {
          return { offset: 0, limit: initialSize };
        }
        return { offset: offset + limit, limit: nextSize };
      }
    }),
    infiniteCursor: (startingCursor) => ({
      type: PAGINATION.TYPE.CURSOR,
      mode: PAGINATION.MODE.INFINITE,
      startingCursor,
      getCursor: (r) => r.cursor,
      reduceResults: (mem, l) => [...mem, ...l]
    })
  }
};

const every = (predicate, collection) => {
  for (let i = 0; i < collection.length; i++) {
    if (!predicate(collection[i])) {
      return false;
    }
  }
  return true;
};

const any = (predicate, collection) => {
  for (let i = 0; i < collection.length; i++) {
    if (predicate(collection[i])) {
      return true;
    }
  }
  return false;
};

class Retriever {
  constructor({ type, name, getter, getProps, publishData, publishError }) {
    this.type = type;
    this.name = name;
    this.getter = getter;
    this.getProps = getProps;
    this.publishData = publishData;
    this.publishError = publishError;
  }

  get() {
    const promise = this.getter(this.getProps());
    if (!promise || !promise.then) {
      throw new Error(`${this.type} for ${this.name} did not return a promise!`);
    }
    return promise.then(this.publishData, this.publishError);
  }

  // eslint-disable-next-line class-methods-use-this
  mergeProps(props) {
    return props;
  }

  // eslint-disable-next-line class-methods-use-this
  onDestroy() {}
}

class ResolveRetriever extends Retriever {
  constructor(args) {
    super({ type: 'resolve', ...args });
  }
}

class PollRetriever extends Retriever {
  constructor(args) {
    super({ type: 'poll', ...args });
    this.interval = args.interval ? setInterval(() => this.get(), args.interval) : null;
  }

  onDestroy() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
  }
}

class ObserveRetriever extends Retriever {
  constructor(args) {
    super({ type: 'observe', ...args });

    this.subscription = null;
  }

  get() {
    const observable = this.getter(this.getProps());
    if (!observable || !observable) {
      throw new Error(`${this.type} for ${this.name} did not expose a subscribe function`);
    }
    this.subscription = observable.subscribe(this.publishData, this.publishError);
    return Promise.resolve();
  }

  onDestroy() {
    if (this.subscription && this.subscription.unsubscribe) {
      this.subscription.unsubscribe();
    }
  }
}

const mergePaginateProps = (props, name, obj) => ({
  ...props,
  paginate: {
    ...props.paginate,
    [name]: obj
  }
});

class PaginatedInfiniteCursorRetriever extends Retriever {
  constructor({ pagerConfig, ...args}) {
    super({ type: 'resolve with cursor', ...args });

    this.pagerConfig = pagerConfig;

    this.pastCursors = [];
    this.nextCursors = pagerConfig.startingCursor || null;

    this.hasNext = true;

    this.pending = null;
  }

  get() {
    if (this.pending) {
      return this.pending;
    }

    const props = this.getProps();

    this.pending = Promise.all([
      ...this.pastCursors.map((cursor) => this.getter(props, cursor)),
      this.getter(props, this.nextCursor).then(response => {
        const nextCursor = this.pagerConfig.getCursor(response);

        this.pastCursors.push(this.nextCursor);
        this.nextCursor = nextCursor;
        this.hasNext = !!nextCursor;
        this.pending = null;

        return response;
      })
    ]).then((results) => this.publishData(results.reduce(this.pagerConfig.reduceResults)));

    this.pending.catch((err) => {
      this.pending = null;
      return Promise.reject(err);
    });

    return this.pending;
  }

  mergeProps(props) {
    return mergePaginateProps(props, this.name, {
      getNext: () => this.get(),
      hasNext: this.hasNext
    });
  }
}

class PaginatedInfiniteOffsetAndLimitResolveRetriever extends Retriever {
  constructor({ pagerConfig, ...args }) {
    super({ type: 'resolve paginated', ...args });
    this.pagerConfig = pagerConfig;

    this.pagers = [];

    this.queueNext();
  }

  get() {
    const props = this.getProps();
    return Promise.all(this.pagers.map((pager) => this.getter(props, pager))).then(
      (lists) => this.publishData(lists.reduce((mem, list) => [...mem, ...list], [])),
      (err) => Promise.reject(err)
    );
  }

  queueNext() {
    const prevPager = this.pagers[this.pagers.length - 1] || { limit: null, offset: null };
    const nextPager = this.pagerConfig.getNextPage(prevPager);
    this.pagers.push(nextPager);
  }

  mergeProps(props) {
    return mergePaginateProps(props, this.name, {
      getNext: () => {
        this.queueNext();
        return this.get();
      }
    });
  }
}

class PaginatedInfiniteOffsetAndLimitObserveRetriever extends Retriever {
  constructor({ pagerConfig, ...args }) {
    super({ type: 'observe paginated', ...args });
    this.pagerConfig = pagerConfig;

    this.pagerSubscriptions = [];

    this.queueNext();
  }

  get() {
    const props = this.getProps();
    return new Promise((resolve) => {
      const tryResolve = (d) => {
        if (every(p_ => p_.data, this.pagerSubscriptions)) {
          resolve(d);
        }
      };
      this.pagerSubscriptions = this.pagerSubscriptions.map((p) => {
        if (!p.subscription) {
          p.firstLoad = true;
          p.subscription = this.getter(props, p.pager).subscribe(
            (data) => {
              p.data = data;
              p.firstLoad = false;
              this.tryToPublish();
              tryResolve(data);
            },
            this.publishError
          );
        }
        return p;
      });
    });
  }

  queueNext() {
    const pagers = this.pagerSubscriptions.map((p) => p.pager);
    const prevPager = pagers[pagers.length - 1] || { limit: null, offset: null };
    const nextPager = this.pagerConfig.getNextPage(prevPager);
    const p = {
      pager: nextPager,
      subscription: null,
      data: null,
      error: null
    };
    this.pagerSubscriptions.push(p);
  }

  tryToPublish() {
    const result = [];
    for (let i = 0; i < this.pagerSubscriptions.length; i++) {
      const p = this.pagerSubscriptions[i];
      if (!p.data) {
        return;
      }
      result.push(...p.data);
    }
    this.publishData(result);
  }

  mergeProps(props) {
    return mergePaginateProps(props, this.name, {
      getNext: () => {
        this.queueNext();
        return this.get();
      },
      isLoading: any(p => !p.data, this.pagerSubscriptions)
    });
  }

  onDestroy() {
    this.pagerSubscriptions.forEach((p) => {
      if (p.subscription && p.subscription.unsubscribe) {
        p.subscription.unsubscribe();
      }
    });
  }
}

const isInfiniteOffsetAndLimitPager = ({ mode, type }) => {
  return mode === PAGINATION.MODE.INFINITE && type === PAGINATION.TYPE.OFFSET_AND_LIMIT;
};

const isInfiniteCursor = ({ mode, type }) => {
  return mode === PAGINATION.MODE.INFINITE && type === PAGINATION.TYPE.CURSOR;
};

const getResolveRetriever = (pagerConfig) => {
  if (pagerConfig) {
    if (isInfiniteOffsetAndLimitPager(pagerConfig)) {
      return PaginatedInfiniteOffsetAndLimitResolveRetriever;
    }

    if (isInfiniteCursor(pagerConfig)) {
      return PaginatedInfiniteCursorRetriever;
    }
  }
  return ResolveRetriever;
};

const getObserveRetriever = (pagerConfig) => {
  if (pagerConfig) {
    if (isInfiniteOffsetAndLimitPager(pagerConfig)) {
      return PaginatedInfiniteOffsetAndLimitObserveRetriever;
    }
  }
  return ObserveRetriever;
};

class Container extends Component {
  constructor(props) {
    super(props);

    this.resolvedData = {};
    this.resolvedDataTargetSize = 0;

    this.retrievers = {};

    this.subscriptions = [];
    this.pagers = {};

    this.isUnmounting = false;

    this.state = {
      pending: false,
      error: null,
      resolvedProps: null
    };
  }

  destroy() {
    Object.keys(this.retrievers).forEach((key) => {
      this.retrievers[key].onDestroy();
      delete this.retrievers[key];
    });
  }

  componentWillMount() {
    this.setupRetrievers(this.props);
    this.trigger();
  }

  componentWillReceiveProps(newProps) {
    const { shouldRefetch = (() => true) } = newProps;
    if (!shouldRefetch(this.props.originalProps, newProps.originalProps)) {
      return;
    }
    this.destroy();
    this.setupRetrievers(newProps);
    this.trigger();
  }

  componentWillUnmount() {
    this.isUnmounting = true;
    this.destroy();
  }

  addResolvedData(field, data) {
    this.resolvedData[field] = data;
    if (this.isUnmounting) {
      return;
    }
    if (this.resolvedDataTargetSize === Object.keys(this.resolvedData).length) {
      this.setState({
        pending: false,
        resolvedProps: { ...this.resolvedData },
        error: null
      });
    }
  }

  setError(field, error) {
    this.setState({ pending: false, error });
  }

  setupRetrievers(props) {
    const { resolve = {}, observe = {}, poll = {}, paginate = {}, originalProps } = props;
    const resolveKeys = Object.keys(resolve);
    const observeKeys = Object.keys(observe);
    const pollKeys = Object.keys(poll);

    const getProps = () => originalProps;
    const publishData = (key) => (data) => this.addResolvedData(key, data);
    const publishError = (key) => (err) => this.setError(key, err);

    resolveKeys.forEach((key) => {
      const pagerConfig = paginate[key];
      const Constructor = getResolveRetriever(pagerConfig);
      this.retrievers[key] = new Constructor({
        name: key,
        publishData: publishData(key),
        publishError: publishError(key),
        getProps,
        getter: resolve[key],
        pagerConfig
      });
    });

    observeKeys.forEach((key) => {
      const pagerConfig = paginate[key];
      const Constructor = getObserveRetriever(pagerConfig);
      this.retrievers[key] = new Constructor({
        name: key,
        publishData: publishData(key),
        publishError: publishError(key),
        getProps,
        getter: observe[key],
        pagerConfig
      });
    });

    pollKeys.forEach((key) => {
      const getter = poll[key].resolve;
      const interval = (poll[key].interval || (() => null))(originalProps);
      this.retrievers[key] = new PollRetriever({
        name: key,
        publishData: publishData(key),
        publishError: publishError(key),
        getProps,
        getter,
        interval
      });
    });

    this.resolvedDataTargetSize = resolveKeys.length + observeKeys.length + pollKeys.length;
  }

  trigger() {
    this.setState({ pending: true, error: null });

    Object.keys(this.retrievers).forEach((key) => {
      this.retrievers[key].get();
    });
  }

  render() {
    const { pending, error, resolvedProps } = this.state;
    const { originalProps, errorComponent, pendingComponent, component } = this.props;

    if (pending) {
      return pendingComponent ? createElement(pendingComponent, originalProps) : null;
    }

    if (error) {
      return errorComponent ? createElement(errorComponent, { ...originalProps, error }) : null;
    }

    const nextProps = Object.keys(this.retrievers).reduce(
      (props, key) => this.retrievers[key].mergeProps(props),
      { ...originalProps, ...resolvedProps }
    );
    return createElement(component, nextProps);
  }
}

export function withData(conf) {
  return component => {
    // make it pure again
    return (originalProps) => {
      const props = { ...conf, originalProps, component };
      return createElement(Container, props);
    };
  };
}

withData.PAGINATION = PAGINATION;

<commit_msg>refactor(with-data) Make unmount guard more robust<commit_after>import { createElement, Component } from 'react';

const PAGINATION = {
  TYPE: {
    OFFSET_AND_LIMIT: 'offsetAndLimit',
    CURSOR: 'cursor'
  },
  MODE: {
    INFINITE: 'infinite'
  },
  PRESET: {
    infiniteOffsetAndLimit: (initialSize, nextSize = initialSize) => ({
      type: PAGINATION.TYPE.OFFSET_AND_LIMIT,
      mode: PAGINATION.MODE.INFINITE,
      getNextPage: ({ limit, offset }) => {
        if (offset === null) {
          return { offset: 0, limit: initialSize };
        }
        return { offset: offset + limit, limit: nextSize };
      }
    }),
    infiniteCursor: (startingCursor) => ({
      type: PAGINATION.TYPE.CURSOR,
      mode: PAGINATION.MODE.INFINITE,
      startingCursor,
      getCursor: (r) => r.cursor,
      reduceResults: (mem, l) => [...mem, ...l]
    })
  }
};

const every = (predicate, collection) => {
  for (let i = 0; i < collection.length; i++) {
    if (!predicate(collection[i])) {
      return false;
    }
  }
  return true;
};

const any = (predicate, collection) => {
  for (let i = 0; i < collection.length; i++) {
    if (predicate(collection[i])) {
      return true;
    }
  }
  return false;
};

class Retriever {
  constructor({ type, name, getter, getProps, publishData, publishError }) {
    this.type = type;
    this.name = name;
    this.getter = getter;
    this.getProps = getProps;
    this.publishData = publishData;
    this.publishError = publishError;
  }

  get() {
    const promise = this.getter(this.getProps());
    if (!promise || !promise.then) {
      throw new Error(`${this.type} for ${this.name} did not return a promise!`);
    }
    return promise.then(this.publishData, this.publishError);
  }

  // eslint-disable-next-line class-methods-use-this
  mergeProps(props) {
    return props;
  }

  // eslint-disable-next-line class-methods-use-this
  onDestroy() {}
}

class ResolveRetriever extends Retriever {
  constructor(args) {
    super({ type: 'resolve', ...args });
  }
}

class PollRetriever extends Retriever {
  constructor(args) {
    super({ type: 'poll', ...args });
    this.interval = args.interval ? setInterval(() => this.get(), args.interval) : null;
  }

  onDestroy() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
  }
}

class ObserveRetriever extends Retriever {
  constructor(args) {
    super({ type: 'observe', ...args });

    this.subscription = null;
  }

  get() {
    const observable = this.getter(this.getProps());
    if (!observable || !observable) {
      throw new Error(`${this.type} for ${this.name} did not expose a subscribe function`);
    }
    this.subscription = observable.subscribe(this.publishData, this.publishError);
    return Promise.resolve();
  }

  onDestroy() {
    if (this.subscription && this.subscription.unsubscribe) {
      this.subscription.unsubscribe();
    }
  }
}

const mergePaginateProps = (props, name, obj) => ({
  ...props,
  paginate: {
    ...props.paginate,
    [name]: obj
  }
});

class PaginatedInfiniteCursorRetriever extends Retriever {
  constructor({ pagerConfig, ...args}) {
    super({ type: 'resolve with cursor', ...args });

    this.pagerConfig = pagerConfig;

    this.pastCursors = [];
    this.nextCursors = pagerConfig.startingCursor || null;

    this.hasNext = true;

    this.pending = null;
  }

  get() {
    if (this.pending) {
      return this.pending;
    }

    const props = this.getProps();

    this.pending = Promise.all([
      ...this.pastCursors.map((cursor) => this.getter(props, cursor)),
      this.getter(props, this.nextCursor).then(response => {
        const nextCursor = this.pagerConfig.getCursor(response);

        this.pastCursors.push(this.nextCursor);
        this.nextCursor = nextCursor;
        this.hasNext = !!nextCursor;
        this.pending = null;

        return response;
      })
    ]).then((results) => this.publishData(results.reduce(this.pagerConfig.reduceResults)));

    this.pending.catch((err) => {
      this.pending = null;
      return Promise.reject(err);
    });

    return this.pending;
  }

  mergeProps(props) {
    return mergePaginateProps(props, this.name, {
      getNext: () => this.get(),
      hasNext: this.hasNext
    });
  }
}

class PaginatedInfiniteOffsetAndLimitResolveRetriever extends Retriever {
  constructor({ pagerConfig, ...args }) {
    super({ type: 'resolve paginated', ...args });
    this.pagerConfig = pagerConfig;

    this.pagers = [];

    this.queueNext();
  }

  get() {
    const props = this.getProps();
    return Promise.all(this.pagers.map((pager) => this.getter(props, pager))).then(
      (lists) => this.publishData(lists.reduce((mem, list) => [...mem, ...list], [])),
      (err) => Promise.reject(err)
    );
  }

  queueNext() {
    const prevPager = this.pagers[this.pagers.length - 1] || { limit: null, offset: null };
    const nextPager = this.pagerConfig.getNextPage(prevPager);
    this.pagers.push(nextPager);
  }

  mergeProps(props) {
    return mergePaginateProps(props, this.name, {
      getNext: () => {
        this.queueNext();
        return this.get();
      }
    });
  }
}

class PaginatedInfiniteOffsetAndLimitObserveRetriever extends Retriever {
  constructor({ pagerConfig, ...args }) {
    super({ type: 'observe paginated', ...args });
    this.pagerConfig = pagerConfig;

    this.pagerSubscriptions = [];

    this.queueNext();
  }

  get() {
    const props = this.getProps();
    return new Promise((resolve) => {
      const tryResolve = (d) => {
        if (every(p_ => p_.data, this.pagerSubscriptions)) {
          resolve(d);
        }
      };
      this.pagerSubscriptions = this.pagerSubscriptions.map((p) => {
        if (!p.subscription) {
          p.firstLoad = true;
          p.subscription = this.getter(props, p.pager).subscribe(
            (data) => {
              p.data = data;
              p.firstLoad = false;
              this.tryToPublish();
              tryResolve(data);
            },
            this.publishError
          );
        }
        return p;
      });
    });
  }

  queueNext() {
    const pagers = this.pagerSubscriptions.map((p) => p.pager);
    const prevPager = pagers[pagers.length - 1] || { limit: null, offset: null };
    const nextPager = this.pagerConfig.getNextPage(prevPager);
    const p = {
      pager: nextPager,
      subscription: null,
      data: null,
      error: null
    };
    this.pagerSubscriptions.push(p);
  }

  tryToPublish() {
    const result = [];
    for (let i = 0; i < this.pagerSubscriptions.length; i++) {
      const p = this.pagerSubscriptions[i];
      if (!p.data) {
        return;
      }
      result.push(...p.data);
    }
    this.publishData(result);
  }

  mergeProps(props) {
    return mergePaginateProps(props, this.name, {
      getNext: () => {
        this.queueNext();
        return this.get();
      },
      isLoading: any(p => !p.data, this.pagerSubscriptions)
    });
  }

  onDestroy() {
    this.pagerSubscriptions.forEach((p) => {
      if (p.subscription && p.subscription.unsubscribe) {
        p.subscription.unsubscribe();
      }
    });
  }
}

const isInfiniteOffsetAndLimitPager = ({ mode, type }) => {
  return mode === PAGINATION.MODE.INFINITE && type === PAGINATION.TYPE.OFFSET_AND_LIMIT;
};

const isInfiniteCursor = ({ mode, type }) => {
  return mode === PAGINATION.MODE.INFINITE && type === PAGINATION.TYPE.CURSOR;
};

const getResolveRetriever = (pagerConfig) => {
  if (pagerConfig) {
    if (isInfiniteOffsetAndLimitPager(pagerConfig)) {
      return PaginatedInfiniteOffsetAndLimitResolveRetriever;
    }

    if (isInfiniteCursor(pagerConfig)) {
      return PaginatedInfiniteCursorRetriever;
    }
  }
  return ResolveRetriever;
};

const getObserveRetriever = (pagerConfig) => {
  if (pagerConfig) {
    if (isInfiniteOffsetAndLimitPager(pagerConfig)) {
      return PaginatedInfiniteOffsetAndLimitObserveRetriever;
    }
  }
  return ObserveRetriever;
};

class Container extends Component {
  constructor(props) {
    super(props);

    this.resolvedData = {};
    this.resolvedDataTargetSize = 0;

    this.retrievers = {};

    this.subscriptions = [];
    this.pagers = {};

    this.isUnmounting = false;

    this.state = {
      pending: false,
      error: null,
      resolvedProps: null
    };
  }

  destroy() {
    Object.keys(this.retrievers).forEach((key) => {
      this.retrievers[key].onDestroy();
      delete this.retrievers[key];
    });
  }

  componentWillMount() {
    this.setupRetrievers(this.props);
    this.trigger();
  }

  componentWillReceiveProps(newProps) {
    const { shouldRefetch = (() => true) } = newProps;
    if (!shouldRefetch(this.props.originalProps, newProps.originalProps)) {
      return;
    }
    this.destroy();
    this.setupRetrievers(newProps);
    this.trigger();
  }

  componentWillUnmount() {
    this.isUnmounting = true;
    this.destroy();
  }

  safeSetState(...args) {
    if (!this.isUnmounting) {
      this.setState(...args);
    }
  }

  addResolvedData(field, data) {
    this.resolvedData[field] = data;
    if (this.resolvedDataTargetSize === Object.keys(this.resolvedData).length) {
      this.safeSetState({
        pending: false,
        resolvedProps: { ...this.resolvedData },
        error: null
      });
    }
  }

  setError(field, error) {
    this.safeSetState({ pending: false, error });
  }

  setupRetrievers(props) {
    const { resolve = {}, observe = {}, poll = {}, paginate = {}, originalProps } = props;
    const resolveKeys = Object.keys(resolve);
    const observeKeys = Object.keys(observe);
    const pollKeys = Object.keys(poll);

    const getProps = () => originalProps;
    const publishData = (key) => (data) => this.addResolvedData(key, data);
    const publishError = (key) => (err) => this.setError(key, err);

    resolveKeys.forEach((key) => {
      const pagerConfig = paginate[key];
      const Constructor = getResolveRetriever(pagerConfig);
      this.retrievers[key] = new Constructor({
        name: key,
        publishData: publishData(key),
        publishError: publishError(key),
        getProps,
        getter: resolve[key],
        pagerConfig
      });
    });

    observeKeys.forEach((key) => {
      const pagerConfig = paginate[key];
      const Constructor = getObserveRetriever(pagerConfig);
      this.retrievers[key] = new Constructor({
        name: key,
        publishData: publishData(key),
        publishError: publishError(key),
        getProps,
        getter: observe[key],
        pagerConfig
      });
    });

    pollKeys.forEach((key) => {
      const getter = poll[key].resolve;
      const interval = (poll[key].interval || (() => null))(originalProps);
      this.retrievers[key] = new PollRetriever({
        name: key,
        publishData: publishData(key),
        publishError: publishError(key),
        getProps,
        getter,
        interval
      });
    });

    this.resolvedDataTargetSize = resolveKeys.length + observeKeys.length + pollKeys.length;
  }

  trigger() {
    this.safeSetState({ pending: true, error: null });

    Object.keys(this.retrievers).forEach((key) => {
      this.retrievers[key].get();
    });
  }

  render() {
    const { pending, error, resolvedProps } = this.state;
    const { originalProps, errorComponent, pendingComponent, component } = this.props;

    if (pending) {
      return pendingComponent ? createElement(pendingComponent, originalProps) : null;
    }

    if (error) {
      return errorComponent ? createElement(errorComponent, { ...originalProps, error }) : null;
    }

    const nextProps = Object.keys(this.retrievers).reduce(
      (props, key) => this.retrievers[key].mergeProps(props),
      { ...originalProps, ...resolvedProps }
    );
    return createElement(component, nextProps);
  }
}

export function withData(conf) {
  return component => {
    // make it pure again
    return (originalProps) => {
      const props = { ...conf, originalProps, component };
      return createElement(Container, props);
    };
  };
}

withData.PAGINATION = PAGINATION;

<|endoftext|>


<commit_before>'use strict';

(function() {

    angular
        .module('simpleArmoryApp')
        .controller('ApplicationCtrl' , ApplicationCtrl);

    function ApplicationCtrl($scope, LoginService, $location, $filter) {

        // default to not logged in
        $scope.isLoggedIn = false;

        // Listen for path changed and then parse and fetch the character
        $scope.$on('$locationChangeSuccess', function(){
            // If there was an error we need to reset everything
            if ($location.$$path === '/error') {
                $scope.character = null;
                $scope.isLoggedIn = false;
            } else if ($location.$$path !== '' && $location.$$path !== '/') {
                // "us/proudmoore/marko"
                // [0]: us/proudmoore/marko
                // [1]: spirestone
                // [2]: marko
                // [3]: location part
                var rgr = new RegExp('([^\/]+)/([^\/]+)/([^\/]+)/?([^\/]+)?').exec($location.$$path);
                rgr = rgr ? rgr : {};

                LoginService.getCharacter({'region': rgr[1], 'realm':rgr[2], 'character':rgr[3]})
                    .then(function(character) {
                        $scope.character = character[0];
                        $scope.isLoggedIn = true;
                });
            }
        });

        // Helper function for percentage numbers.  Used in a lot of screens
        $scope.percent = function(n, d) {

            return $filter('number')(((n / d) * 100), 0);
        };

        $scope.achFormater = function(n, d) {
            if (!n || !d) {
                return "";
            }

            return '' + n + ' / ' + d + ' (' + $scope.percent(n, d) + '%)';
        };

        // Helper to get the image id off an item
        $scope.getImageSrc = function(item) {
            if (item.collected) {
                // wowhead img
                return 'http://wow.zamimg.com/images/wow/icons/medium/' + item.icon.toLowerCase() + '.jpg';
            } else {
                // 1x1 gif
                return 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
            }
        };
  }

})();<commit_msg>fix battle pets total<commit_after>'use strict';

(function() {

    angular
        .module('simpleArmoryApp')
        .controller('ApplicationCtrl' , ApplicationCtrl);

    function ApplicationCtrl($scope, LoginService, $location, $filter) {

        // default to not logged in
        $scope.isLoggedIn = false;

        // Listen for path changed and then parse and fetch the character
        $scope.$on('$locationChangeSuccess', function(){
            // If there was an error we need to reset everything
            if ($location.$$path === '/error') {
                $scope.character = null;
                $scope.isLoggedIn = false;
            } else if ($location.$$path !== '' && $location.$$path !== '/') {
                // "us/proudmoore/marko"
                // [0]: us/proudmoore/marko
                // [1]: spirestone
                // [2]: marko
                // [3]: location part
                var rgr = new RegExp('([^\/]+)/([^\/]+)/([^\/]+)/?([^\/]+)?').exec($location.$$path);
                rgr = rgr ? rgr : {};

                LoginService.getCharacter({'region': rgr[1], 'realm':rgr[2], 'character':rgr[3]})
                    .then(function(character) {
                        $scope.character = character[0];
                        $scope.isLoggedIn = true;
                });
            }
        });

        // Helper function for percentage numbers.  Used in a lot of screens
        $scope.percent = function(n, d) {

            return $filter('number')(((n / d) * 100), 0);
        };

        $scope.achFormater = function(n, d) {
            if (!n || !d) {
                return '';
            }

            var perc = $scope.percent(n, d);

            // if the percentage is low enough, don't print the numbers, just use the percentage
            if (perc < 18) {
                return perc + '%';
            }

            return '' + n + ' / ' + d + ' (' + perc + '%)';
        };

        // Helper to get the image id off an item
        $scope.getImageSrc = function(item) {
            if (item.collected) {
                // wowhead img
                return 'http://wow.zamimg.com/images/wow/icons/medium/' + item.icon.toLowerCase() + '.jpg';
            } else {
                // 1x1 gif
                return 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
            }
        };
  }

})();<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc function
 * @name ng311.controller:ServiceRequestCreateCtrl
 * @description
 * # ServiceRequestCreateCtrl
 * ServiceRequest create controller of ng311
 */
angular
  .module('ng311')
  .controller('ServiceRequestCreateCtrl', function (
    $rootScope,
    $scope,
    $state,
    $stateParams,
    Account,
    ServiceRequest,
    endpoints,
    party) {

    //action performed by this controller
    $scope.action = 'Create';

    $scope.edit = true;

    $scope.groups = endpoints.servicegroups.servicegroups;
    $scope.jurisdictions = endpoints.jurisdictions.jurisdictions;
    $scope.services = endpoints.services.services;
    $scope.methods = party.settings.servicerequest.methods;

    //instantiate new servicerequest
    var servicerequest = _.merge({}, {
      _id: ($stateParams || {})._id,
      call: {
        startedAt: new Date()
      },
      reporter: ($stateParams || {}).reporter || {},
      jurisdiction: ($stateParams || {}).jurisdiction,
      service: ($stateParams || {}).service,
      description: ($stateParams || {}).description,
      address: ($stateParams || {}).address,
      method: _.merge({}, { name: undefined }, ($stateParams || {}).method)
    });

    $scope.servicerequest = new ServiceRequest(servicerequest);

    /**
     * @description save created servicerequest
     */
    $scope.save = function () {

      $scope.create = false;
      $scope.updated = true;

      //set call end time
      if (!$scope.servicerequest._id) {
        $scope.servicerequest.call.endedAt = new Date();
      }

      //ensure operator on attending
      if (!$scope.servicerequest.operator) {
        $scope.servicerequest.operator = party ? party._id : undefined;
      }

      //try update or save servicerequest
      var updateOrSave =
        (!$scope.servicerequest._id ?
          $scope.servicerequest.$save() : $scope.servicerequest.$update());

      updateOrSave.then(function (response) {

        response = response || {};

        response.message =
          response.message || 'Service Request Saved Successfully';

        $rootScope.$broadcast('appSuccess', response);

        $rootScope.$broadcast('servicerequest:create:success', response);

        $rootScope.$broadcast('app:servicerequests:reload');

        $state.go('app.servicerequests.list');

      })
        .catch(function (error) {
          $rootScope.$broadcast('appError', error);
          $rootScope.$broadcast('servicerequest:create:error', error);
        });
    };

    /**
     * @description Launch a customer lookup details in a modal window
     */
    $scope.openLookupModal = function () {

      var accountNumber = $scope.servicerequest.reporter.account;

      Account
        .getDetails(accountNumber)
        .then(function (account) {
          console.log(account);
          $rootScope.account = account;
          $state.go('account.details');
        });
    };

  });
<commit_msg>Service Request: Allow outopopulation of service request form based on account details<commit_after>'use strict';

/**
 * @ngdoc function
 * @name ng311.controller:ServiceRequestCreateCtrl
 * @description
 * # ServiceRequestCreateCtrl
 * ServiceRequest create controller of ng311
 */
angular
  .module('ng311')
  .controller('ServiceRequestCreateCtrl', function (
    $rootScope,
    $scope,
    $state,
    $stateParams,
    Account,
    ServiceRequest,
    endpoints,
    party) {

    //action performed by this controller
    $scope.action = 'Create';

    $scope.edit = true;

    $scope.groups = endpoints.servicegroups.servicegroups;
    $scope.jurisdictions = endpoints.jurisdictions.jurisdictions;
    $scope.services = endpoints.services.services;
    $scope.methods = party.settings.servicerequest.methods;

    //instantiate new servicerequest
    var servicerequest = _.merge({}, {
      _id: ($stateParams || {})._id,
      call: {
        startedAt: new Date()
      },
      reporter: ($stateParams || {}).reporter || {},
      jurisdiction: ($stateParams || {}).jurisdiction,
      service: ($stateParams || {}).service,
      description: ($stateParams || {}).description,
      address: ($stateParams || {}).address,
      method: _.merge({}, { name: undefined }, ($stateParams || {}).method)
    });

    $scope.servicerequest = new ServiceRequest(servicerequest);

    /**
     * @description save created servicerequest
     */
    $scope.save = function () {

      $scope.create = false;
      $scope.updated = true;

      //set call end time
      if (!$scope.servicerequest._id) {
        $scope.servicerequest.call.endedAt = new Date();
      }

      //ensure operator on attending
      if (!$scope.servicerequest.operator) {
        $scope.servicerequest.operator = party ? party._id : undefined;
      }

      //try update or save servicerequest
      var updateOrSave =
        (!$scope.servicerequest._id ?
          $scope.servicerequest.$save() : $scope.servicerequest.$update());

      updateOrSave.then(function (response) {

        response = response || {};

        response.message =
          response.message || 'Service Request Saved Successfully';

        $rootScope.$broadcast('appSuccess', response);

        $rootScope.$broadcast('servicerequest:create:success', response);

        $rootScope.$broadcast('app:servicerequests:reload');

        $state.go('app.servicerequests.list');

      })
        .catch(function (error) {
          $rootScope.$broadcast('appError', error);
          $rootScope.$broadcast('servicerequest:create:error', error);
        });
    };

    /**
     * @description Launch a customer lookup details in a modal window
     */
    $scope.openLookupModal = function () {

      var accountNumber = $scope.servicerequest.reporter.account;

      Account
        .getDetails(accountNumber)
        .then(function (account) {
          account = account || {};
          $rootScope.account = account;
          $scope.servicerequest.reporter = _.merge({}, {
            name: account.name,
            email: account.email
          }, $scope.servicerequest.reporter);

          $scope.servicerequest.jurisdiction =
            $scope.servicerequest.jurisdiction || account.jurisdiction;
          $scope.servicerequest.address =
            $scope.servicerequest.address || account.address;
          $scope.servicerequest.location = account.location;
          $state.go('account.details');
        });
    };

  });
<|endoftext|>


<commit_before>/**
 * GeoKnow Generator
 *    Configuration management
 *
 * @author Vadim Zaslawski
 *
 * ISQL:
 *   GRANT SPARQL_UPDATE TO "SPARQL"
 *   GRANT EXECUTE ON DB.DBA.L_O_LOOK TO "SPARQL"
 *
 * usage:
 *   CONFIG.setEndpoint(url)          - set SPARQL endpoint, should be called initially
 *   CONFIG.getEndpoint()             - get SPARQL endpoint
 *   CONFIG.getBase()                 - get base URL
 *   CONFIG.getGraph()                - get graph name
 *   CONFIG.read([success(settings)]) - load settings
 *   CONFIG.getSettings()             - get loaded settings
 *   CONFIG.write()                   - save settings after editing CONFIG.getSettings() result
 *   CONFIG.select(property, value)   - select settings, e.g.: CONFIG.select("rdf:type", "lds:StackComponent")
 *   CONFIG.createGraph(name)         - create graph
 *   CONFIG.dropGraph(name)           - delete graph
 */

"use strict";

var CONFIG = CONFIG || (function()
{
	var endpoint  = "http://localhost:8890/sparql"
	,	BASE      = "http://generator.geoknow.eu/"
	,	GRAPH_URI = BASE + "settingsGraph"
	,	NS        = GRAPH_URI + "#";

	var namespaces =
	{
		"http://dbpedia.org/resource/"                     : "dbpedia:"
	,	"http://purl.org/dc/elements/1.1/"                 : "dc:"
	,	"http://xmlns.com/foaf/0.1/"                       : "foaf:"
	,	"http://linkeddata.org/integrated-stack-schema/"   : "lds:"
	,	"http://www.w3.org/1999/02/22-rdf-syntax-ns#"      : "rdf:"
	,	"http://www.w3.org/2000/01/rdf-schema#"            : "rdfs:"
	,	"http://www.w3.org/ns/sparql-service-description#" : "sd:"
	};
	namespaces[NS] = ":";

	var NAMESPACES = "PREFIX : <" + NS + ">"
	,	GRAPH      = "<" + GRAPH_URI + ">"
	,	FORMAT     = "application/sparql-results+json"
	,	EOL        = "\r\n";

	var settings = {};

	var isLoaded = false;

	var ns = function(v)
	{
		var value = v.value || v;

		if (!v.type || v.type == "uri")
		{
			var namespace = /.*[#\/]/.exec(value);
			if (namespace)
			{
				var prefix = namespaces[namespace = namespace[0]];
				if (prefix)
					return prefix + value.slice(namespace.length);
			}
		}

		return value;
	}

	return {
		setEndpoint: function(url)
		{
			endpoint = url;
		}

		getEndpoint: function()
		{
			return endpoint;
		}

	,	getBase: function()
		{
			return BASE;
		}

	,	getGraph: function()
		{
			return GRAPH;
		}

	,	getSettings: function()
		{
			return settings;
		}

	,	select: function(property, value)
		{
			property = ns(property);
			value    = ns(value);

			var elements = {};

			for (var resource in settings)
			{
				var element = settings[resource];
				var prop = element[property];
				if (prop)
					for (var i in prop)
						if (prop[i] == value)
						{
							elements[resource] = element;
							break;
						}
			}

			return elements;
		}

	,	read: function(success)
		{
			if (isLoaded)
			{
				if (success)
					try
					{
						success(settings);
					}
					catch (e)
					{
						console.error(e);
					}
				return;
			}

			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: "SELECT * FROM " + GRAPH + EOL
				+	"WHERE { ?s ?p ?o }" + EOL
				+	"ORDER BY ?s ?p ?o"
				}
			,	function(data)
				{
					try
					{
						var bindings = data.results.bindings
						,	triples  = [];

						for (var i in bindings)
						{
							var binding = bindings[i];
							triples.push([ ns(binding.s), ns(binding.p), ns(binding.o) ]);
						}

						settings = {};

						var bnodes = {};

						for (var i in triples)
						{
							var triple = triples[i]
							,	s = triple[0]
							,	p = triple[1]
							,	o = triple[2];

							if (/^nodeID:\/\//.test(s))
							{
								var id = s.slice(9);
								var map = bnodes[id] = bnodes[id] || {};
							}
							else
							{
								var map = settings[s] || (settings[s] = {});

								if (/^nodeID:\/\//.test(o))
								{
									var id = o.slice(9);
									o = bnodes[id] = bnodes[id] || {};
								}
							}

							(map[p] || (map[p] = [])).push(o);
						}

						isLoaded = true;

						if (success)
							success(settings);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}

	,	write: function()
		{
			var wrap = function(s)
			{
				return /^http:\/\//.test(s) ? "<" + s + ">" : !/^\w*:/.test(s) ? '"' + s + '"' : s;
			}

			var data = "",
				bnodeIndex = 0;

			var walk = function(s, map)
			{
				for (var p in map)
				{
					var arr = map[p];
					for (var i in arr)
					{
						var o = arr[i];
						if (typeof o === "string")
							data += wrap(s) + " " + wrap(p) + " " + wrap(o) + " ." + EOL;
						else
						{
							var bnode = "_:b" + ++bnodeIndex;
							data += wrap(s) + " " + wrap(p) + " " + bnode + " ." + EOL;
							walk(bnode, o);
						}
					}
				}
			}

			for (var s in settings)
				walk(s, settings[s]);

			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: NAMESPACES + EOL
					+	"DROP SILENT GRAPH "   + GRAPH + EOL
					+	"CREATE SILENT GRAPH " + GRAPH + EOL
					+	"INSERT INTO "         + GRAPH + EOL
					+	"{" + EOL
					+	data
					+	"}"
				}
			,	function(data)
				{
					try
					{
						console.log(data.results.bindings[0]["callret-0"].value);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}

	,	createGraph: function(name)
		{
			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: "CREATE SILENT GRAPH " + name
				}
			,	function(data)
				{
					try
					{
						console.log(data.results.bindings[0]["callret-0"].value);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}

	,	dropGraph: function(name)
		{
			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: "DROP SILENT GRAPH " + name
				}
			,	function(data)
				{
					try
					{
						console.log(data.results.bindings[0]["callret-0"].value);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}
	};
})();
<commit_msg>Namespaces updated.<commit_after>/**
 * GeoKnow Generator
 *    Configuration management
 *
 * @author Vadim Zaslawski
 *
 * ISQL:
 *   GRANT SPARQL_UPDATE TO "SPARQL"
 *   GRANT EXECUTE ON DB.DBA.L_O_LOOK TO "SPARQL"
 *
 * usage:
 *   CONFIG.setEndpoint(url)          - set SPARQL endpoint, should be called initially
 *   CONFIG.getEndpoint()             - get SPARQL endpoint
 *   CONFIG.getNS()                   - get base namespace
 *   CONFIG.getGraph()                - get graph name
 *   CONFIG.read([success(settings)]) - load settings
 *   CONFIG.getSettings()             - get loaded settings
 *   CONFIG.write()                   - save settings after editing CONFIG.getSettings() result
 *   CONFIG.select(property, value)   - select settings, e.g.: CONFIG.select("rdf:type", "lds:StackComponent")
 *   CONFIG.createGraph(name)         - create graph
 *   CONFIG.dropGraph(name)           - delete graph
 */

"use strict";

var CONFIG = CONFIG || (function()
{
	var endpoint  = "http://localhost:8890/sparql"
	,	NS        = "http://generator.geoknow.eu/"
	,	GRAPH_URI = NS + "settingsGraph";

	var namespaces =
	{
		"http://dbpedia.org/resource/"                     : "dbpedia:"
	,	"http://purl.org/dc/elements/1.1/"                 : "dc:"
	,	"http://xmlns.com/foaf/0.1/"                       : "foaf:"
	,	"http://linkeddata.org/integrated-stack-schema/"   : "lds:"
	,	"http://www.w3.org/1999/02/22-rdf-syntax-ns#"      : "rdf:"
	,	"http://www.w3.org/2000/01/rdf-schema#"            : "rdfs:"
	,	"http://www.w3.org/ns/sparql-service-description#" : "sd:"
	};
	namespaces[NS] = ":";

	var NAMESPACES = "PREFIX : <" + NS + ">"
	,	GRAPH      = "<" + GRAPH_URI + ">"
	,	FORMAT     = "application/sparql-results+json"
	,	EOL        = "\r\n";

	var settings = {};

	var isLoaded = false;

	var ns = function(v)
	{
		var value = v.value || v;

		if (!v.type || v.type == "uri")
		{
			var namespace = /.*[#\/]/.exec(value);
			if (namespace)
			{
				var prefix = namespaces[namespace = namespace[0]];
				if (prefix)
					return prefix + value.slice(namespace.length);
			}
		}

		return value;
	}

	return {
		setEndpoint: function(url)
		{
			endpoint = url;
		}

		getEndpoint: function()
		{
			return endpoint;
		}

	,	getNS: function()
		{
			return NS;
		}

	,	getGraph: function()
		{
			return GRAPH;
		}

	,	getSettings: function()
		{
			return settings;
		}

	,	select: function(property, value)
		{
			property = ns(property);
			value    = ns(value);

			var elements = {};

			for (var resource in settings)
			{
				var element = settings[resource];
				var prop = element[property];
				if (prop)
					for (var i in prop)
						if (prop[i] == value)
						{
							elements[resource] = element;
							break;
						}
			}

			return elements;
		}

	,	read: function(success)
		{
			if (isLoaded)
			{
				if (success)
					try
					{
						success(settings);
					}
					catch (e)
					{
						console.error(e);
					}
				return;
			}

			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: "SELECT * FROM " + GRAPH + EOL
				+	"WHERE { ?s ?p ?o }" + EOL
				+	"ORDER BY ?s ?p ?o"
				}
			,	function(data)
				{
					try
					{
						var bindings = data.results.bindings
						,	triples  = [];

						for (var i in bindings)
						{
							var binding = bindings[i];
							triples.push([ ns(binding.s), ns(binding.p), ns(binding.o) ]);
						}

						settings = {};

						var bnodes = {};

						for (var i in triples)
						{
							var triple = triples[i]
							,	s = triple[0]
							,	p = triple[1]
							,	o = triple[2];

							if (/^nodeID:\/\//.test(s))
							{
								var id = s.slice(9);
								var map = bnodes[id] = bnodes[id] || {};
							}
							else
							{
								var map = settings[s] || (settings[s] = {});

								if (/^nodeID:\/\//.test(o))
								{
									var id = o.slice(9);
									o = bnodes[id] = bnodes[id] || {};
								}
							}

							(map[p] || (map[p] = [])).push(o);
						}

						isLoaded = true;

						if (success)
							success(settings);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}

	,	write: function()
		{
			var wrap = function(s)
			{
				return /^http:\/\//.test(s) ? "<" + s + ">" : !/^\w*:/.test(s) ? '"' + s + '"' : s;
			}

			var data = "",
				bnodeIndex = 0;

			var walk = function(s, map)
			{
				for (var p in map)
				{
					var arr = map[p];
					for (var i in arr)
					{
						var o = arr[i];
						if (typeof o === "string")
							data += wrap(s) + " " + wrap(p) + " " + wrap(o) + " ." + EOL;
						else
						{
							var bnode = "_:b" + ++bnodeIndex;
							data += wrap(s) + " " + wrap(p) + " " + bnode + " ." + EOL;
							walk(bnode, o);
						}
					}
				}
			}

			for (var s in settings)
				walk(s, settings[s]);

			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: NAMESPACES + EOL
					+	"DROP SILENT GRAPH "   + GRAPH + EOL
					+	"CREATE SILENT GRAPH " + GRAPH + EOL
					+	"INSERT INTO "         + GRAPH + EOL
					+	"{" + EOL
					+	data
					+	"}"
				}
			,	function(data)
				{
					try
					{
						console.log(data.results.bindings[0]["callret-0"].value);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}

	,	createGraph: function(name)
		{
			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: "CREATE SILENT GRAPH " + name
				}
			,	function(data)
				{
					try
					{
						console.log(data.results.bindings[0]["callret-0"].value);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}

	,	dropGraph: function(name)
		{
			$.getJSON(endpoint + "?callback=?",
				{
					format: FORMAT
				,	query: "DROP SILENT GRAPH " + name
				}
			,	function(data)
				{
					try
					{
						console.log(data.results.bindings[0]["callret-0"].value);
					}
					catch (e)
					{
						console.error(e);
					}
				}
			);
		}
	};
})();
<|endoftext|>